diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx index cc25ac920899..eee127d51fdd 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx @@ -159,7 +159,7 @@ export function ShowShareModal({ if (_g?.filters && _g.filters.length === 0) { _g = omit(_g, 'filters'); } - const baseUrl = setStateToKbnUrl('_g', _g); + const baseUrl = setStateToKbnUrl('_g', _g, undefined, window.location.href); const shareableUrl = setStateToKbnUrl( '_a', diff --git a/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts b/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts index 752ee39724de..79e6742a793c 100644 --- a/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts +++ b/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts @@ -11,10 +11,10 @@ import { History } from 'history'; import { getQueryParams, - replaceUrlHashQuery, IKbnUrlStateStorage, createQueryParamObservable, } from '@kbn/kibana-utils-plugin/public'; +import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common'; import type { Query } from '@kbn/es-query'; import { SearchSessionInfoProvider } from '@kbn/data-plugin/public'; diff --git a/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts b/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts index b0d37de482de..6d90316098fb 100644 --- a/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts +++ b/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts @@ -10,7 +10,8 @@ import _ from 'lodash'; import { debounceTime } from 'rxjs/operators'; import semverSatisfies from 'semver/functions/satisfies'; -import { IKbnUrlStateStorage, replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/public'; +import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common'; import { DashboardPanelMap, diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 84689c8feecc..fa457c425440 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -33,8 +33,8 @@ import type { import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common'; import { createKbnUrlTracker } from '@kbn/kibana-utils-plugin/public'; -import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/public'; import type { SavedObjectsStart } from '@kbn/saved-objects-plugin/public'; import type { VisualizationsStart } from '@kbn/visualizations-plugin/public'; import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 208871f77022..811222ef71e8 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -210,7 +210,9 @@ export const EditIndexPattern = withRouter( {indexPatternHeading} - {indexPattern.title} + + {indexPattern.title} + )} @@ -218,7 +220,9 @@ export const EditIndexPattern = withRouter( {timeFilterHeading} - {indexPattern.timeFieldName} + + {indexPattern.timeFieldName} + )} diff --git a/src/plugins/data_views/public/mocks.ts b/src/plugins/data_views/public/mocks.ts index 61db42e18a9b..2178cf450300 100644 --- a/src/plugins/data_views/public/mocks.ts +++ b/src/plugins/data_views/public/mocks.ts @@ -37,6 +37,7 @@ const createStartContract = (): Start => { getCanSaveSync: jest.fn(), getIdsWithTitle: jest.fn(), getFieldsForIndexPattern: jest.fn(), + create: jest.fn().mockReturnValue(Promise.resolve({})), } as unknown as jest.Mocked; }; diff --git a/src/plugins/discover/common/constants.ts b/src/plugins/discover/common/constants.ts index 76cf8b8f8f3c..8e61baa8ba6f 100644 --- a/src/plugins/discover/common/constants.ts +++ b/src/plugins/discover/common/constants.ts @@ -8,3 +8,7 @@ export const DEFAULT_ROWS_PER_PAGE = 100; export const ROWS_PER_PAGE_OPTIONS = [10, 25, 50, DEFAULT_ROWS_PER_PAGE, 250, 500]; +export enum VIEW_MODE { + DOCUMENT_LEVEL = 'documents', + AGGREGATED_LEVEL = 'aggregated', +} diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 1e204683c0cf..97180412d67f 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -30,3 +30,6 @@ export const SEARCH_EMBEDDABLE_TYPE = 'search'; export const HIDE_ANNOUNCEMENTS = 'hideAnnouncements'; export const SHOW_LEGACY_FIELD_TOP_VALUES = 'discover:showLegacyFieldTopValues'; export const ENABLE_SQL = 'discover:enableSql'; + +export { DISCOVER_APP_LOCATOR, DiscoverAppLocatorDefinition } from './locator'; +export type { DiscoverAppLocator, DiscoverAppLocatorParams } from './locator'; diff --git a/src/plugins/discover/public/locator.test.ts b/src/plugins/discover/common/locator.test.ts similarity index 97% rename from src/plugins/discover/public/locator.test.ts rename to src/plugins/discover/common/locator.test.ts index 221c7a4958fa..9152b99072de 100644 --- a/src/plugins/discover/public/locator.test.ts +++ b/src/plugins/discover/common/locator.test.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import { hashedItemStore, getStatesFromKbnUrl } from '@kbn/kibana-utils-plugin/public'; +import { + hashedItemStore, + getStatesFromKbnUrl, + setStateToKbnUrl, +} from '@kbn/kibana-utils-plugin/public'; import { mockStorage } from '@kbn/kibana-utils-plugin/public/storage/hashed_item_store/mock'; import { FilterStateStore } from '@kbn/es-query'; import { DiscoverAppLocatorDefinition } from './locator'; @@ -20,7 +24,7 @@ interface SetupParams { } const setup = async ({ useHash = false }: SetupParams = {}) => { - const locator = new DiscoverAppLocatorDefinition({ useHash }); + const locator = new DiscoverAppLocatorDefinition({ useHash, setStateToKbnUrl }); return { locator, diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/common/locator.ts similarity index 88% rename from src/plugins/discover/public/locator.ts rename to src/plugins/discover/common/locator.ts index e803433c4d70..77e501dbe40d 100644 --- a/src/plugins/discover/public/locator.ts +++ b/src/plugins/discover/common/locator.ts @@ -10,9 +10,9 @@ import type { SerializableRecord } from '@kbn/utility-types'; import type { Filter, TimeRange, Query, AggregateQuery } from '@kbn/es-query'; import type { GlobalQueryStateFromUrl, RefreshInterval } from '@kbn/data-plugin/public'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; -import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; -import { DataViewSpec } from '@kbn/data-views-plugin/public'; -import type { VIEW_MODE } from './components/view_mode_toggle'; +import { DataViewSpec } from '@kbn/data-views-plugin/common'; +import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; +import { VIEW_MODE } from './constants'; export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR'; @@ -95,12 +95,17 @@ export interface DiscoverAppLocatorParams extends SerializableRecord { * Breakdown field */ breakdownField?: string; + /** + * Used when navigating to particular alert results + */ + isAlertResults?: boolean; } export type DiscoverAppLocator = LocatorPublic; export interface DiscoverAppLocatorDependencies { useHash: boolean; + setStateToKbnUrl: typeof setStateToKbnUrl; } /** @@ -108,6 +113,7 @@ export interface DiscoverAppLocatorDependencies { */ export interface MainHistoryLocationState { dataViewSpec?: DataViewSpec; + isAlertResults?: boolean; } export class DiscoverAppLocatorDefinition implements LocatorDefinition { @@ -134,6 +140,7 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition('_g', queryState, { useHash }, path); - path = setStateToKbnUrl('_a', appState, { useHash }, path); + path = this.deps.setStateToKbnUrl('_g', queryState, { useHash }, path); + path = this.deps.setStateToKbnUrl('_a', appState, { useHash }, path); if (searchSessionId) { path = `${path}&searchSessionId=${searchSessionId}`; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 56ef50615b25..c9f52bd548f2 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -23,6 +23,7 @@ import { isOfQueryType } from '@kbn/es-query'; import classNames from 'classnames'; import { generateFilters } from '@kbn/data-plugin/public'; import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; +import { VIEW_MODE } from '../../../../../common/constants'; import { useInternalStateSelector } from '../../services/discover_internal_state_container'; import { useAppStateSelector } from '../../services/discover_app_state_container'; import { useInspector } from '../../hooks/use_inspector'; @@ -41,7 +42,6 @@ import { DataMainMsg, RecordRawType } from '../../hooks/use_saved_search'; import { useColumns } from '../../../../hooks/use_data_grid_columns'; import { FetchStatus } from '../../../types'; import { useDataState } from '../../hooks/use_data_state'; -import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { hasActiveFilter } from './utils'; import { getRawRecordType } from '../../utils/get_raw_record_type'; import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout'; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index b085e61f348d..f91de3b2a02d 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -11,9 +11,10 @@ import { SavedSearch } from '@kbn/saved-search-plugin/public'; import React, { useCallback } from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; import { METRIC_TYPE } from '@kbn/analytics'; +import { VIEW_MODE } from '../../../../../common/constants'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DataTableRecord } from '../../../../types'; -import { DocumentViewModeToggle, VIEW_MODE } from '../../../../components/view_mode_toggle'; +import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'; import { DataRefetch$, SavedSearchData } from '../../hooks/use_saved_search'; import { DiscoverStateContainer } from '../../services/discover_state'; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx index 8799e82d7e8a..e688f1120526 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx @@ -20,12 +20,12 @@ import type { AggregateQuery, Query } from '@kbn/es-query'; import { getDefaultFieldFilter } from './lib/field_filter'; import { createDiscoverServicesMock } from '../../../../__mocks__/services'; import { stubLogstashDataView } from '@kbn/data-plugin/common/stubs'; -import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../../types'; import { AvailableFields$, DataDocuments$ } from '../../hooks/use_saved_search'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; +import { VIEW_MODE } from '../../../../../common/constants'; import { DiscoverMainProvider } from '../../services/discover_state_provider'; import * as ExistingFieldsHookApi from '@kbn/unified-field-list-plugin/public/hooks/use_existing_fields'; import { ExistenceFetchStatus } from '@kbn/unified-field-list-plugin/public'; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index 75bfdffa7962..51cdaadd1cda 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -27,6 +27,7 @@ import { triggerVisualizeActionsTextBasedLanguages, useGroupedFields, } from '@kbn/unified-field-list-plugin/public'; +import { VIEW_MODE } from '../../../../../common/constants'; import { useAppStateSelector } from '../../services/discover_app_state_container'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverField } from './discover_field'; @@ -40,7 +41,6 @@ import { } from './lib/group_fields'; import { doesFieldMatchFilters, FieldFilterState, setFieldFilterProp } from './lib/field_filter'; import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; -import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { getUiActions } from '../../../../kibana_services'; import { getRawRecordType } from '../../utils/get_raw_record_type'; import { RecordRawType } from '../../hooks/use_saved_search'; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index 6a1c2af61bcf..3a56fcb8c211 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -22,10 +22,10 @@ import { DiscoverServices } from '../../../../build_services'; import { FetchStatus } from '../../../types'; import { AvailableFields$, DataDocuments$, RecordRawType } from '../../hooks/use_saved_search'; import { stubLogstashDataView } from '@kbn/data-plugin/common/stubs'; -import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { DiscoverAppStateProvider } from '../../services/discover_app_state_container'; +import { VIEW_MODE } from '../../../../../common/constants'; import * as ExistingFieldsServiceApi from '@kbn/unified-field-list-plugin/public/services/field_existing/load_field_existing'; import { resetExistingFieldsCache } from '@kbn/unified-field-list-plugin/public/hooks/use_existing_fields'; import { createDiscoverServicesMock } from '../../../../__mocks__/services'; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index e89efc988235..89ccef3cde93 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -28,11 +28,12 @@ import { useExistingFieldsFetcher, useQuerySubscriber, } from '@kbn/unified-field-list-plugin/public'; +import { VIEW_MODE } from '../../../../../common/constants'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { AvailableFields$, DataDocuments$, RecordRawType } from '../../hooks/use_saved_search'; -import { VIEW_MODE } from '../../../../components/view_mode_toggle'; +import { calcFieldCounts } from '../../utils/calc_field_counts'; import { FetchStatus } from '../../../types'; import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../../../../components/discover_tour'; import { getRawRecordType } from '../../utils/get_raw_record_type'; @@ -43,7 +44,6 @@ import { DiscoverSidebarReducerActionType, DiscoverSidebarReducerStatus, } from './lib/sidebar_reducer'; -import { calcFieldCounts } from '../../utils/calc_field_counts'; export interface DiscoverSidebarResponsiveProps { /** diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index a50c596b4dfd..b317c0e5cffd 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -21,6 +21,7 @@ import { getSavedSearch, getSavedSearchFullPathUrl, } from '@kbn/saved-search-plugin/public'; +import { MainHistoryLocationState } from '../../../common/locator'; import { getDiscoverStateContainer } from './services/discover_state'; import { loadDataView, resolveDataView } from './utils/resolve_data_view'; import { DiscoverMainApp } from './discover_main_app'; @@ -30,7 +31,7 @@ import { DiscoverError } from '../../components/common/error_alert'; import { useDiscoverServices } from '../../hooks/use_discover_services'; import { getScopedHistory, getUrlTracker } from '../../kibana_services'; import { restoreStateFromSavedSearch } from '../../services/saved_searches/restore_from_saved_search'; -import { MainHistoryLocationState } from '../../locator'; +import { useAlertResultsToast } from './hooks/use_alert_results_toast'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); @@ -72,6 +73,11 @@ export function DiscoverMainRoute(props: Props) { [] ); + useAlertResultsToast({ + isAlertResults: historyLocationState?.isAlertResults, + toastNotifications, + }); + useExecutionContext(core.executionContext, { type: 'application', page: 'app', diff --git a/src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx b/src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx new file mode 100644 index 000000000000..424cd554ecd1 --- /dev/null +++ b/src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToastsStart } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { MarkdownSimple, toMountPoint } from '@kbn/kibana-react-plugin/public'; +import React, { useEffect } from 'react'; + +export const displayPossibleDocsDiffInfoAlert = (toastNotifications: ToastsStart) => { + const infoTitle = i18n.translate('discover.viewAlert.documentsMayVaryInfoTitle', { + defaultMessage: 'Displayed documents may vary', + }); + const infoDescription = i18n.translate('discover.viewAlert.documentsMayVaryInfoDescription', { + defaultMessage: `The displayed documents might differ from the documents that triggered the alert. + Some documents might have been added or deleted.`, + }); + + toastNotifications.addInfo({ + title: infoTitle, + text: toMountPoint({infoDescription}), + }); +}; + +export const useAlertResultsToast = ({ + isAlertResults, + toastNotifications, +}: { + isAlertResults?: boolean; + toastNotifications: ToastsStart; +}) => { + useEffect(() => { + if (isAlertResults) { + displayPossibleDocsDiffInfoAlert(toastNotifications); + } + }, [isAlertResults, toastNotifications]); +}; diff --git a/src/plugins/discover/public/application/main/services/discover_app_state_container.ts b/src/plugins/discover/public/application/main/services/discover_app_state_container.ts index ddfd7950f6eb..0c1c94f72eb9 100644 --- a/src/plugins/discover/public/application/main/services/discover_app_state_container.ts +++ b/src/plugins/discover/public/application/main/services/discover_app_state_container.ts @@ -11,8 +11,8 @@ import { ReduxLikeStateContainer, } from '@kbn/kibana-utils-plugin/common'; import { AggregateQuery, Filter, Query } from '@kbn/es-query'; +import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; import { DiscoverGridSettings } from '../../../components/discover_grid/types'; -import { VIEW_MODE } from '../../../components/view_mode_toggle'; export interface AppState { /** diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index b4a1bcbf0f1f..52e0d6e72fbf 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -29,6 +29,7 @@ import { } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; +import { DiscoverAppLocatorParams, DISCOVER_APP_LOCATOR } from '../../../../common'; import { AppState } from './discover_app_state_container'; import { getInternalStateContainer, @@ -37,7 +38,6 @@ import { import { getStateDefaults } from '../utils/get_state_defaults'; import { DiscoverServices } from '../../../build_services'; import { handleSourceColumnState } from '../../../utils/state_helpers'; -import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '../../../locator'; import { cleanupUrlState } from '../utils/cleanup_url_state'; import { getValidFilters } from '../../../utils/get_valid_filters'; diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index 0fdd58c05823..84e65e8d672d 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -8,41 +8,19 @@ import { useEffect, useMemo } from 'react'; import { useHistory, useLocation, useParams } from 'react-router-dom'; -import { sha256 } from 'js-sha256'; -import type { Rule } from '@kbn/alerting-plugin/common'; -import { getTime } from '@kbn/data-plugin/common'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import type { Filter } from '@kbn/es-query'; -import { DiscoverAppLocatorParams } from '../../locator'; +import { DiscoverAppLocatorParams } from '../../../common/locator'; import { useDiscoverServices } from '../../hooks/use_discover_services'; -import { getAlertUtils, QueryParams, SearchThresholdAlertParams } from './view_alert_utils'; +import { displayPossibleDocsDiffInfoAlert } from '../main/hooks/use_alert_results_toast'; +import { getAlertUtils, QueryParams } from './view_alert_utils'; -type NonNullableEntry = { [K in keyof T]: NonNullable }; +const DISCOVER_MAIN_ROUTE = '/'; -const getCurrentChecksum = (params: SearchThresholdAlertParams) => - sha256.create().update(JSON.stringify(params)).hex(); +type NonNullableEntry = { [K in keyof T]: NonNullable }; const isActualAlert = (queryParams: QueryParams): queryParams is NonNullableEntry => { - return Boolean(queryParams.from && queryParams.to && queryParams.checksum); -}; - -const buildTimeRangeFilter = ( - dataView: DataView, - fetchedAlert: Rule, - timeFieldName: string -) => { - const filter = getTime(dataView, { - from: `now-${fetchedAlert.params.timeWindowSize}${fetchedAlert.params.timeWindowUnit}`, - to: 'now', - }); - return { - from: filter?.query.range[timeFieldName].gte, - to: filter?.query.range[timeFieldName].lte, - }; + return Boolean(queryParams.from && queryParams.to); }; -const DISCOVER_MAIN_ROUTE = '/'; - export function ViewAlertRoute() { const { core, data, locator, toastNotifications } = useDiscoverServices(); const { id } = useParams<{ id: string }>(); @@ -55,100 +33,39 @@ export function ViewAlertRoute() { () => ({ from: query.get('from'), to: query.get('to'), - checksum: query.get('checksum'), }), [query] ); + /** + * This flag indicates whether we should open the actual alert results or current state of documents. + */ const openActualAlert = useMemo(() => isActualAlert(queryParams), [queryParams]); useEffect(() => { - const { - fetchAlert, - fetchSearchSource, - displayRuleChangedWarn, - displayPossibleDocsDiffInfoAlert, - showDataViewFetchError, - showDataViewUpdatedWarning, - } = getAlertUtils(toastNotifications, core, data); - - const navigateToResults = async () => { - const fetchedAlert = await fetchAlert(id); - if (!fetchedAlert) { - history.push(DISCOVER_MAIN_ROUTE); - return; - } - const fetchedSearchSource = await fetchSearchSource(fetchedAlert); - if (!fetchedSearchSource) { - history.push(DISCOVER_MAIN_ROUTE); - return; - } - const dataView = fetchedSearchSource.getField('index'); - const timeFieldName = dataView?.timeFieldName; - // data view fetch error - if (!dataView || !timeFieldName) { - showDataViewFetchError(fetchedAlert.id); - history.push(DISCOVER_MAIN_ROUTE); - return; + const { fetchAlert, fetchSearchSource, buildLocatorParams } = getAlertUtils( + openActualAlert, + queryParams, + toastNotifications, + core, + data + ); + + const navigateWithDiscoverState = (state: DiscoverAppLocatorParams) => { + if (openActualAlert) { + displayPossibleDocsDiffInfoAlert(toastNotifications); } - - if (dataView.isPersisted()) { - const dataViewSavedObject = await core.savedObjects.client.get( - 'index-pattern', - dataView.id! - ); - - const alertUpdatedAt = fetchedAlert.updatedAt; - const dataViewUpdatedAt = dataViewSavedObject.updatedAt!; - // data view updated after the last update of the alert rule - if ( - openActualAlert && - new Date(dataViewUpdatedAt).valueOf() > new Date(alertUpdatedAt).valueOf() - ) { - showDataViewUpdatedWarning(); - } - } - - const calculatedChecksum = getCurrentChecksum(fetchedAlert.params); - // rule params changed - if (openActualAlert && calculatedChecksum !== queryParams.checksum) { - displayRuleChangedWarn(); - } else if (openActualAlert && calculatedChecksum === queryParams.checksum) { - // documents might be updated or deleted - displayPossibleDocsDiffInfoAlert(); - } - - const timeRange = openActualAlert - ? { from: queryParams.from, to: queryParams.to } - : buildTimeRangeFilter(dataView, fetchedAlert, timeFieldName); - const state: DiscoverAppLocatorParams = { - query: fetchedSearchSource.getField('query') || data.query.queryString.getDefaultQuery(), - dataViewSpec: dataView.toSpec(false), - timeRange, - }; - - const filters = fetchedSearchSource.getField('filter'); - if (filters) { - state.filters = filters as Filter[]; - } - - await locator.navigate(state); + locator.navigate(state); }; - navigateToResults(); - }, [ - toastNotifications, - data.query.queryString, - data.search.searchSource, - core.http, - locator, - id, - queryParams, - history, - openActualAlert, - core, - data, - ]); + const navigateToDiscoverRoot = () => history.push(DISCOVER_MAIN_ROUTE); + + fetchAlert(id) + .then(fetchSearchSource) + .then(buildLocatorParams) + .then(navigateWithDiscoverState) + .catch(navigateToDiscoverRoot); + }, [core, data, history, id, locator, openActualAlert, queryParams, toastNotifications]); return null; } diff --git a/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx b/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx index d5b6aac22d3b..f29fd7d40b51 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx @@ -9,11 +9,14 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { CoreStart, ToastsStart } from '@kbn/core/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; import type { Rule } from '@kbn/alerting-plugin/common'; import type { RuleTypeParams } from '@kbn/alerting-plugin/common'; -import { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; +import { ISearchSource, SerializedSearchSourceFields, getTime } from '@kbn/data-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { MarkdownSimple, toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { Filter } from '@kbn/es-query'; +import { DiscoverAppLocatorParams } from '../../../common/locator'; export interface SearchThresholdAlertParams extends RuleTypeParams { searchConfiguration: SerializedSearchSourceFields; @@ -22,12 +25,28 @@ export interface SearchThresholdAlertParams extends RuleTypeParams { export interface QueryParams { from: string | null; to: string | null; - checksum: string | null; } const LEGACY_BASE_ALERT_API_PATH = '/api/alerts'; +const buildTimeRangeFilter = ( + dataView: DataView, + fetchedAlert: Rule, + timeFieldName: string +) => { + const filter = getTime(dataView, { + from: `now-${fetchedAlert.params.timeWindowSize}${fetchedAlert.params.timeWindowUnit}`, + to: 'now', + }); + return { + from: filter?.query.range[timeFieldName].gte, + to: filter?.query.range[timeFieldName].lte, + }; +}; + export const getAlertUtils = ( + openActualAlert: boolean, + queryParams: QueryParams, toastNotifications: ToastsStart, core: CoreStart, data: DataPublicPluginStart @@ -46,36 +65,6 @@ export const getAlertUtils = ( }); }; - const displayRuleChangedWarn = () => { - const warnTitle = i18n.translate('discover.viewAlert.alertRuleChangedWarnTitle', { - defaultMessage: 'Alert rule has changed', - }); - const warnDescription = i18n.translate('discover.viewAlert.alertRuleChangedWarnDescription', { - defaultMessage: `The displayed documents might not match the documents that triggered the alert - because the rule configuration changed.`, - }); - - toastNotifications.addWarning({ - title: warnTitle, - text: toMountPoint({warnDescription}), - }); - }; - - const displayPossibleDocsDiffInfoAlert = () => { - const infoTitle = i18n.translate('discover.viewAlert.documentsMayVaryInfoTitle', { - defaultMessage: 'Displayed documents may vary', - }); - const infoDescription = i18n.translate('discover.viewAlert.documentsMayVaryInfoDescription', { - defaultMessage: `The displayed documents might differ from the documents that triggered the alert. - Some documents might have been added or deleted.`, - }); - - toastNotifications.addInfo({ - title: infoTitle, - text: toMountPoint({infoDescription}), - }); - }; - const fetchAlert = async (id: string) => { try { return await core.http.get>( @@ -89,12 +78,18 @@ export const getAlertUtils = ( title: errorTitle, text: toMountPoint({error.message}), }); + throw new Error(errorTitle); } }; const fetchSearchSource = async (fetchedAlert: Rule) => { try { - return await data.search.searchSource.create(fetchedAlert.params.searchConfiguration); + return { + alert: fetchedAlert, + searchSource: await data.search.searchSource.create( + fetchedAlert.params.searchConfiguration + ), + }; } catch (error) { const errorTitle = i18n.translate('discover.viewAlert.searchSourceErrorTitle', { defaultMessage: 'Error fetching search source', @@ -103,29 +98,40 @@ export const getAlertUtils = ( title: errorTitle, text: toMountPoint({error.message}), }); + throw new Error(errorTitle); } }; - const showDataViewUpdatedWarning = async () => { - const warnTitle = i18n.translate('discover.viewAlert.dataViewChangedWarnTitle', { - defaultMessage: 'Data View has changed', - }); - const warnDescription = i18n.translate('discover.viewAlert.dataViewChangedWarnDescription', { - defaultMessage: `Data view has been updated after the last update of the alert rule.`, - }); + const buildLocatorParams = ({ + alert, + searchSource, + }: { + alert: Rule; + searchSource: ISearchSource; + }): DiscoverAppLocatorParams => { + const dataView = searchSource.getField('index'); + const timeFieldName = dataView?.timeFieldName; + // data view fetch error + if (!dataView || !timeFieldName) { + showDataViewFetchError(alert.id); + throw new Error('Data view fetch error'); + } - toastNotifications.addWarning({ - title: warnTitle, - text: toMountPoint({warnDescription}), - }); + const timeRange = openActualAlert + ? { from: queryParams.from, to: queryParams.to } + : buildTimeRangeFilter(dataView, alert, timeFieldName); + + return { + query: searchSource.getField('query') || data.query.queryString.getDefaultQuery(), + dataViewSpec: dataView.toSpec(false), + timeRange, + filters: searchSource.getField('filter') as Filter[], + }; }; return { fetchAlert, fetchSearchSource, - displayRuleChangedWarn, - displayPossibleDocsDiffInfoAlert, - showDataViewFetchError, - showDataViewUpdatedWarning, + buildLocatorParams, }; }; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 27f5d59b07e1..5975805522f4 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -47,11 +47,11 @@ import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plug import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; -import { DiscoverAppLocator } from './locator'; import { getHistory } from './kibana_services'; import { DiscoverStartPlugins } from './plugin'; import { DiscoverContextAppLocator } from './application/context/services/locator'; import { DiscoverSingleDocLocator } from './application/doc/locator'; +import { DiscoverAppLocator } from '../common'; /** * Location state of internal Discover history instance diff --git a/src/plugins/discover/public/components/view_mode_toggle/constants.ts b/src/plugins/discover/public/components/view_mode_toggle/constants.ts deleted file mode 100644 index d03c0710d12b..000000000000 --- a/src/plugins/discover/public/components/view_mode_toggle/constants.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export enum VIEW_MODE { - DOCUMENT_LEVEL = 'documents', - AGGREGATED_LEVEL = 'aggregated', -} diff --git a/src/plugins/discover/public/components/view_mode_toggle/index.ts b/src/plugins/discover/public/components/view_mode_toggle/index.ts index 95b76f5879d1..06aab965b97b 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/index.ts +++ b/src/plugins/discover/public/components/view_mode_toggle/index.ts @@ -7,4 +7,3 @@ */ export { DocumentViewModeToggle } from './view_mode_toggle'; -export { VIEW_MODE } from './constants'; diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx index 450d7c2816d7..7c17e5e1a31e 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx @@ -7,10 +7,10 @@ */ import { EuiTab } from '@elastic/eui'; +import { VIEW_MODE } from '../../../common/constants'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; -import { VIEW_MODE } from './constants'; import { DocumentViewModeToggle } from './view_mode_toggle'; describe('Document view mode toggle component', () => { diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx index 99c7403c3fbf..befe62ef42fb 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; -import { VIEW_MODE } from './constants'; +import { VIEW_MODE } from '../../../common/constants'; import { SHOW_FIELD_STATISTICS } from '../../../common'; import { useDiscoverServices } from '../../hooks/use_discover_services'; diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts index 77bb0d98fd21..0e30156381ad 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts @@ -20,8 +20,8 @@ import { of, throwError } from 'rxjs'; import { ReactWrapper } from 'enzyme'; import { SHOW_FIELD_STATISTICS } from '../../common'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import { VIEW_MODE } from '../components/view_mode_toggle'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; +import { VIEW_MODE } from '../../common/constants'; let discoverComponent: ReactWrapper; diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 81e25fd3c6b8..96c23d085afc 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -35,6 +35,7 @@ import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { METRIC_TYPE } from '@kbn/analytics'; +import { VIEW_MODE } from '../../common/constants'; import { getSortForEmbeddable, SortPair } from '../utils/sorting'; import { RecordRawType } from '../application/main/hooks/use_saved_search'; import { buildDataTableRecord } from '../utils/build_data_record'; @@ -56,7 +57,6 @@ import { handleSourceColumnState } from '../utils/state_helpers'; import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; import { DiscoverGridSettings } from '../components/discover_grid/types'; import { DocTableProps } from '../components/doc_table/doc_table_wrapper'; -import { VIEW_MODE } from '../components/view_mode_toggle'; import { updateSearchSource } from './utils/update_search_source'; import { FieldStatisticsTable } from '../application/main/components/field_stats_table'; import { getRawRecordType } from '../application/main/utils/get_raw_record_type'; diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 451bf3303216..bafa6b603d6e 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -18,9 +18,6 @@ export type { ISearchEmbeddable, SearchInput } from './embeddable'; export { SEARCH_EMBEDDABLE_TYPE } from './embeddable'; export { loadSharingDataHelpers } from './utils'; -export { DISCOVER_APP_LOCATOR } from './locator'; -export type { DiscoverAppLocator, DiscoverAppLocatorParams } from './locator'; - // re-export types and static functions to give other plugins time to migrate away export { type SavedSearch, diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 407584df5787..1d106309641a 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -40,6 +40,7 @@ import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui- import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; import { PLUGIN_ID } from '../common'; import { DocViewInput, DocViewInputFn } from './services/doc_views/doc_views_types'; @@ -54,7 +55,6 @@ import { } from './kibana_services'; import { registerFeature } from './register_feature'; import { buildServices } from './build_services'; -import { DiscoverAppLocator, DiscoverAppLocatorDefinition } from './locator'; import { SearchEmbeddableFactory } from './embeddable'; import { DeferredSpinner } from './components'; import { ViewSavedSearchAction } from './embeddable/view_saved_search_action'; @@ -70,6 +70,7 @@ import { DiscoverSingleDocLocator, DiscoverSingleDocLocatorDefinition, } from './application/doc/locator'; +import { DiscoverAppLocator, DiscoverAppLocatorDefinition } from '../common'; const DocViewerLegacyTable = React.lazy( () => import('./services/doc_views/components/doc_viewer_table/legacy') @@ -218,7 +219,7 @@ export class DiscoverPlugin if (plugins.share) { const useHash = core.uiSettings.get('state:storeInSessionStorage'); this.locator = plugins.share.url.locators.create( - new DiscoverAppLocatorDefinition({ useHash }) + new DiscoverAppLocatorDefinition({ useHash, setStateToKbnUrl }) ); this.contextLocator = plugins.share.url.locators.create( diff --git a/src/plugins/discover/public/utils/initialize_kbn_url_tracking.ts b/src/plugins/discover/public/utils/initialize_kbn_url_tracking.ts index f53537cd5351..87ffd52d5891 100644 --- a/src/plugins/discover/public/utils/initialize_kbn_url_tracking.ts +++ b/src/plugins/discover/public/utils/initialize_kbn_url_tracking.ts @@ -8,11 +8,8 @@ import { AppUpdater, CoreSetup } from '@kbn/core/public'; import type { BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; -import { - createGetterSetter, - createKbnUrlTracker, - replaceUrlHashQuery, -} from '@kbn/kibana-utils-plugin/public'; +import { createGetterSetter, createKbnUrlTracker } from '@kbn/kibana-utils-plugin/public'; +import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common'; import { getScopedHistory } from '../kibana_services'; import { SEARCH_SESSION_ID_QUERY_PARAM } from '../constants'; import type { DiscoverSetupPlugins } from '../plugin'; diff --git a/src/plugins/discover/server/plugin.ts b/src/plugins/discover/server/plugin.ts index cfd62312fb6d..a2ee1d79b6a1 100644 --- a/src/plugins/discover/server/plugin.ts +++ b/src/plugins/discover/server/plugin.ts @@ -9,9 +9,12 @@ import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; import type { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; import type { HomeServerPluginSetup } from '@kbn/home-plugin/server'; +import { SharePluginSetup } from '@kbn/share-plugin/server'; +import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; import { getUiSettings } from './ui_settings'; import { capabilitiesProvider } from './capabilities_provider'; import { registerSampleData } from './sample_data'; +import { DiscoverAppLocatorDefinition } from '../common/locator'; export class DiscoverServerPlugin implements Plugin { public setup( @@ -19,6 +22,7 @@ export class DiscoverServerPlugin implements Plugin { plugins: { data: DataPluginSetup; home?: HomeServerPluginSetup; + share?: SharePluginSetup; } ) { core.capabilities.registerProvider(capabilitiesProvider); @@ -28,6 +32,12 @@ export class DiscoverServerPlugin implements Plugin { registerSampleData(plugins.home.sampleData); } + if (plugins.share) { + plugins.share.url.locators.create( + new DiscoverAppLocatorDefinition({ useHash: false, setStateToKbnUrl }) + ); + } + return {}; } diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index 8a7d9bfaf4aa..cd06d6c36948 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -31,6 +31,8 @@ export type { PureTransition, CreateStateContainerOptions, } from './state_containers'; +export { setStateToKbnUrl } from './state_management/set_state_to_kbn_url'; +export { replaceUrlHashQuery } from './state_management/format'; export { createStateContainerReactHelpers, useContainerSelector, diff --git a/src/plugins/kibana_utils/common/state_management/encode_state.ts b/src/plugins/kibana_utils/common/state_management/encode_state.ts new file mode 100644 index 000000000000..53026c716bfc --- /dev/null +++ b/src/plugins/kibana_utils/common/state_management/encode_state.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import rison from '@kbn/rison'; + +// should be: +// export function encodeState but this leads to the chain of +// types mismatches up to BaseStateContainer interfaces, as in state containers we don't +// have any restrictions on state shape +export function encodeState( + state: State, + useHash: boolean, + createHash: (rawState: State) => string +): string { + if (useHash) { + return createHash(state); + } else { + return rison.encodeUnknown(state) ?? ''; + } +} diff --git a/src/plugins/kibana_utils/public/state_management/url/format.test.ts b/src/plugins/kibana_utils/common/state_management/format.test.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_management/url/format.test.ts rename to src/plugins/kibana_utils/common/state_management/format.test.ts diff --git a/src/plugins/kibana_utils/public/state_management/url/format.ts b/src/plugins/kibana_utils/common/state_management/format.ts similarity index 95% rename from src/plugins/kibana_utils/public/state_management/url/format.ts rename to src/plugins/kibana_utils/common/state_management/format.ts index d2d9f9faae35..b6279334ceee 100644 --- a/src/plugins/kibana_utils/public/state_management/url/format.ts +++ b/src/plugins/kibana_utils/common/state_management/format.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ +import { ParsedQuery, stringify } from 'query-string'; import { format as formatUrl } from 'url'; -import { stringify, ParsedQuery } from 'query-string'; import { parseUrl, parseUrlHash } from './parse'; -import { url as urlUtils } from '../../../common'; +import { url as urlUtils } from '..'; export function replaceUrlQuery( rawUrl: string, diff --git a/src/plugins/kibana_utils/public/state_management/url/parse.test.ts b/src/plugins/kibana_utils/common/state_management/parse.test.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_management/url/parse.test.ts rename to src/plugins/kibana_utils/common/state_management/parse.test.ts diff --git a/src/plugins/kibana_utils/public/state_management/url/parse.ts b/src/plugins/kibana_utils/common/state_management/parse.ts similarity index 82% rename from src/plugins/kibana_utils/public/state_management/url/parse.ts rename to src/plugins/kibana_utils/common/state_management/parse.ts index d1e95b1c60e0..e1545034fb9a 100644 --- a/src/plugins/kibana_utils/public/state_management/url/parse.ts +++ b/src/plugins/kibana_utils/common/state_management/parse.ts @@ -7,11 +7,10 @@ */ import { parse as _parseUrl } from 'url'; -import { History } from 'history'; export const parseUrl = (url: string) => _parseUrl(url, true); + export const parseUrlHash = (url: string) => { const hash = parseUrl(url).hash; return hash ? parseUrl(hash.slice(1)) : null; }; -export const getCurrentUrl = (history: History) => history.createHref(history.location); diff --git a/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.test.ts b/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.test.ts new file mode 100644 index 000000000000..0d13171810c4 --- /dev/null +++ b/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createSetStateToKbnUrl, setStateToKbnUrl } from './set_state_to_kbn_url'; + +describe('set_state_to_kbn_url', () => { + describe('createSetStateToKbnUrl', () => { + it('should call createHash', () => { + const createHash = jest.fn(() => 'hash'); + const localSetStateToKbnUrl = createSetStateToKbnUrl(createHash); + const url = 'http://localhost:5601/oxf/app/kibana#/yourApp'; + const state = { foo: 'bar' }; + const newUrl = localSetStateToKbnUrl('_s', state, { useHash: true }, url); + expect(createHash).toHaveBeenCalledTimes(1); + expect(createHash).toHaveBeenCalledWith(state); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=hash"` + ); + }); + + it('should not call createHash', () => { + const createHash = jest.fn(); + const localSetStateToKbnUrl = createSetStateToKbnUrl(createHash); + const url = 'http://localhost:5601/oxf/app/kibana#/yourApp'; + const state = { foo: 'bar' }; + const newUrl = localSetStateToKbnUrl('_s', state, { useHash: false }, url); + expect(createHash).not.toHaveBeenCalled(); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=(foo:bar)"` + ); + }); + }); + + describe('setStateToKbnUrl', () => { + const url = 'http://localhost:5601/oxf/app/kibana#/yourApp'; + const state1 = { + testStr: '123', + testNumber: 0, + testObj: { test: '123' }, + testNull: null, + testArray: [1, 2, {}], + }; + const state2 = { + test: '123', + }; + + it('should set expanded state to url', () => { + let newUrl = setStateToKbnUrl('_s', state1, { useHash: false }, url); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=(testArray:!(1,2,()),testNull:!n,testNumber:0,testObj:(test:'123'),testStr:'123')"` + ); + newUrl = setStateToKbnUrl('_s', state2, { useHash: false }, newUrl); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=(test:'123')"` + ); + }); + + it('should set expanded state to url before hash', () => { + let newUrl = setStateToKbnUrl('_s', state1, { useHash: false, storeInHashQuery: false }, url); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana?_s=(testArray:!(1,2,()),testNull:!n,testNumber:0,testObj:(test:'123'),testStr:'123')#/yourApp"` + ); + newUrl = setStateToKbnUrl('_s', state2, { useHash: false, storeInHashQuery: false }, newUrl); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana?_s=(test:'123')#/yourApp"` + ); + }); + + it('should set hashed state to url', () => { + let newUrl = setStateToKbnUrl('_s', state1, { useHash: true }, url); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=h@a897fac"` + ); + newUrl = setStateToKbnUrl('_s', state2, { useHash: true }, newUrl); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana#/yourApp?_s=h@40f94d5"` + ); + }); + + it('should set query to url with storeInHashQuery: false', () => { + let newUrl = setStateToKbnUrl( + '_a', + { tab: 'other' }, + { useHash: false, storeInHashQuery: false }, + 'http://localhost:5601/oxf/app/kibana/yourApp' + ); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana/yourApp?_a=(tab:other)"` + ); + newUrl = setStateToKbnUrl( + '_b', + { f: 'test', i: '', l: '' }, + { useHash: false, storeInHashQuery: false }, + newUrl + ); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana/yourApp?_a=(tab:other)&_b=(f:test,i:'',l:'')"` + ); + }); + }); +}); diff --git a/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.ts b/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.ts new file mode 100644 index 000000000000..194e4a1231bd --- /dev/null +++ b/src/plugins/kibana_utils/common/state_management/set_state_to_kbn_url.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { encodeState } from './encode_state'; +import { replaceUrlHashQuery, replaceUrlQuery } from './format'; +import { createStateHash } from './state_hash'; + +export type SetStateToKbnUrlHashOptions = { useHash: boolean; storeInHashQuery?: boolean }; + +export function createSetStateToKbnUrl(createHash: (rawState: State) => string) { + return ( + key: string, + state: State, + { useHash = false, storeInHashQuery = true }: SetStateToKbnUrlHashOptions = { + useHash: false, + storeInHashQuery: true, + }, + rawUrl: string + ): string => { + const replacer = storeInHashQuery ? replaceUrlHashQuery : replaceUrlQuery; + return replacer(rawUrl, (query) => { + const encoded = encodeState(state, useHash, createHash); + return { + ...query, + [key]: encoded, + }; + }); + }; +} + +const internalSetStateToKbnUrl = createSetStateToKbnUrl((rawState: State) => + createStateHash(JSON.stringify(rawState)) +); + +/** + * Common version of setStateToKbnUrl which doesn't use session storage. + * + * Sets state to the url by key and returns a new url string. + * + * e.g.: + * given a url: http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'') + * key: '_a' + * and state: {tab: 'other'} + * + * will return url: + * http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:other)&_b=(f:test,i:'',l:'') + * + * By default due to Kibana legacy reasons assumed that state is stored in a query inside a hash part of the URL: + * http://localhost:5601/oxf/app/kibana#/yourApp?_a={STATE} + * + * { storeInHashQuery: true } option should be used in you want to store you state in a main query (not in a hash): + * http://localhost:5601/oxf/app/kibana?_a={STATE}#/yourApp + */ +export function setStateToKbnUrl( + key: string, + state: State, + hashOptions: SetStateToKbnUrlHashOptions, + rawUrl: string +): string { + return internalSetStateToKbnUrl(key, state, hashOptions, rawUrl); +} diff --git a/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts b/src/plugins/kibana_utils/common/state_management/state_hash.test.ts similarity index 87% rename from src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts rename to src/plugins/kibana_utils/common/state_management/state_hash.test.ts index cd850c25b71c..98c5295541d0 100644 --- a/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts +++ b/src/plugins/kibana_utils/common/state_management/state_hash.test.ts @@ -7,14 +7,9 @@ */ import { encode as encodeRison } from '@kbn/rison'; -import { mockStorage } from '../../storage/hashed_item_store/mock'; import { createStateHash, isStateHash } from './state_hash'; describe('stateHash', () => { - beforeEach(() => { - mockStorage.clear(); - }); - describe('#createStateHash', () => { it('returns a hash', () => { const json = JSON.stringify({ a: 'a' }); @@ -37,6 +32,13 @@ describe('stateHash', () => { const hash2 = createStateHash(json2); expect(hash1).not.toEqual(hash2); }); + + it('calls existingJsonProvider if provided', () => { + const json = JSON.stringify({ a: 'a' }); + const existingJsonProvider = jest.fn(() => json); + createStateHash(json, existingJsonProvider); + expect(existingJsonProvider).toHaveBeenCalled(); + }); }); describe('#isStateHash', () => { diff --git a/src/plugins/kibana_utils/common/state_management/state_hash.ts b/src/plugins/kibana_utils/common/state_management/state_hash.ts new file mode 100644 index 000000000000..811cccc5bfd5 --- /dev/null +++ b/src/plugins/kibana_utils/common/state_management/state_hash.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Sha256 } from '@kbn/crypto-browser'; + +// This prefix is used to identify hash strings that have been encoded in the URL. +const HASH_PREFIX = 'h@'; + +export function isStateHash(str: string) { + return String(str).indexOf(HASH_PREFIX) === 0; +} + +export function createStateHash( + json: string, + existingJsonProvider?: (hash: string) => string | null +) { + if (typeof json !== 'string') { + throw new Error('createHash only accepts strings (JSON).'); + } + + const hash = new Sha256().update(json, 'utf8').digest('hex'); + + let shortenedHash; + + // Shorten the hash to at minimum 7 characters. We just need to make sure that it either: + // a) hasn't been used yet + // b) or has been used already, but with the JSON we're currently hashing. + for (let i = 7; i < hash.length; i++) { + shortenedHash = hash.slice(0, i); + const existingJson = existingJsonProvider ? existingJsonProvider(shortenedHash) : null; + if (existingJson === null || existingJson === json) break; + } + + return `${HASH_PREFIX}${shortenedHash}`; +} diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index d8882f74ee3b..fa50f32247a5 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -71,12 +71,7 @@ export { export type { IStorageWrapper, IStorage } from './storage'; export { Storage } from './storage'; export { hashedItemStore, HashedItemStore } from './storage/hashed_item_store'; -export { - createStateHash, - persistState, - retrieveState, - isStateHash, -} from './state_management/state_hash'; +export { persistState, retrieveState } from './state_management/state_hash'; export { hashQuery, hashUrl, @@ -89,8 +84,6 @@ export { getStatesFromKbnUrl, setStateToKbnUrl, withNotifyOnErrors, - replaceUrlQuery, - replaceUrlHashQuery, } from './state_management/url'; export type { IStateStorage, diff --git a/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts b/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts index 952463b2b7b3..edfb71c32cee 100644 --- a/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts +++ b/src/plugins/kibana_utils/public/state_management/state_encoder/encode_decode_state.ts @@ -7,7 +7,9 @@ */ import rison from '@kbn/rison'; -import { isStateHash, retrieveState, persistState } from '../state_hash'; +import { encodeState } from '../../../common/state_management/encode_state'; +import { isStateHash } from '../../../common/state_management/state_hash'; +import { retrieveState, persistState } from '../state_hash'; // should be: // export function decodeState(expandedOrHashedState: string) @@ -21,21 +23,9 @@ export function decodeState(expandedOrHashedState: string): State { } } -// should be: -// export function encodeState but this leads to the chain of -// types mismatches up to BaseStateContainer interfaces, as in state containers we don't -// have any restrictions on state shape -export function encodeState(state: State, useHash: boolean): string { - if (useHash) { - return persistState(state); - } else { - return rison.encodeUnknown(state) ?? ''; - } -} - export function hashedStateToExpandedState(expandedOrHashedState: string): string { if (isStateHash(expandedOrHashedState)) { - return encodeState(retrieveState(expandedOrHashedState), false); + return encodeState(retrieveState(expandedOrHashedState), false, persistState); } return expandedOrHashedState; diff --git a/src/plugins/kibana_utils/public/state_management/state_encoder/index.ts b/src/plugins/kibana_utils/public/state_management/state_encoder/index.ts index b2174f1b0a3a..0ab6fe580eb0 100644 --- a/src/plugins/kibana_utils/public/state_management/state_encoder/index.ts +++ b/src/plugins/kibana_utils/public/state_management/state_encoder/index.ts @@ -7,7 +7,6 @@ */ export { - encodeState, decodeState, expandedStateToHashedState, hashedStateToExpandedState, diff --git a/src/plugins/kibana_utils/public/state_management/state_hash/index.ts b/src/plugins/kibana_utils/public/state_management/state_hash/index.ts index 1768bfa3181c..adc571b23dd3 100644 --- a/src/plugins/kibana_utils/public/state_management/state_hash/index.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { isStateHash, createStateHash, persistState, retrieveState } from './state_hash'; +export { persistState, retrieveState } from './state_hash'; diff --git a/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts index d8abf1402e9d..03160f7c9f7a 100644 --- a/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts @@ -7,42 +7,9 @@ */ import { i18n } from '@kbn/i18n'; -import { Sha256 } from '@kbn/crypto-browser'; +import { createStateHash } from '../../../common/state_management/state_hash'; import { hashedItemStore } from '../../storage/hashed_item_store'; -// This prefix is used to identify hash strings that have been encoded in the URL. -const HASH_PREFIX = 'h@'; - -export function createStateHash( - json: string, - existingJsonProvider?: (hash: string) => string | null // TODO: temp while state.js relies on this in tests -) { - if (typeof json !== 'string') { - throw new Error('createHash only accepts strings (JSON).'); - } - - const hash = new Sha256().update(json, 'utf8').digest('hex'); - - let shortenedHash; - - // Shorten the hash to at minimum 7 characters. We just need to make sure that it either: - // a) hasn't been used yet - // b) or has been used already, but with the JSON we're currently hashing. - for (let i = 7; i < hash.length; i++) { - shortenedHash = hash.slice(0, i); - const existingJson = existingJsonProvider - ? existingJsonProvider(shortenedHash) - : hashedItemStore.getItem(shortenedHash); - if (existingJson === null || existingJson === json) break; - } - - return `${HASH_PREFIX}${shortenedHash}`; -} - -export function isStateHash(str: string) { - return String(str).indexOf(HASH_PREFIX) === 0; -} - export function retrieveState(stateHash: string): State { const json = hashedItemStore.getItem(stateHash); const throwUnableToRestoreUrlError = () => { @@ -65,7 +32,7 @@ export function retrieveState(stateHash: string): State { export function persistState(state: State): string { const json = JSON.stringify(state); - const hash = createStateHash(json); + const hash = createStateHash(json, hashedItemStore.getItem.bind(hashedItemStore)); const isItemSet = hashedItemStore.setItem(hash, json); if (isItemSet) return hash; diff --git a/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts index 7571c576bcf3..d6e2126ef1d6 100644 --- a/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts +++ b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ +import { replaceUrlHashQuery } from '../../../common/state_management/format'; import { expandedStateToHashedState, hashedStateToExpandedState } from '../state_encoder'; -import { replaceUrlHashQuery } from './format'; export type IParsedUrlQuery = Record; diff --git a/src/plugins/kibana_utils/public/state_management/url/index.ts b/src/plugins/kibana_utils/public/state_management/url/index.ts index 7f39e9ac1b69..5f45b5fee0a7 100644 --- a/src/plugins/kibana_utils/public/state_management/url/index.ts +++ b/src/plugins/kibana_utils/public/state_management/url/index.ts @@ -17,4 +17,3 @@ export { export { createKbnUrlTracker } from './kbn_url_tracker'; export { createUrlTracker } from './url_tracker'; export { withNotifyOnErrors, saveStateInUrlErrorTitle, restoreUrlErrorTitle } from './errors'; -export { replaceUrlHashQuery, replaceUrlQuery } from './format'; diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index 99e3023cae03..b81d3c1b81b6 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -9,10 +9,16 @@ import { format as formatUrl } from 'url'; import { stringify } from 'query-string'; import { createBrowserHistory, History } from 'history'; -import { decodeState, encodeState } from '../state_encoder'; -import { getCurrentUrl, parseUrl, parseUrlHash } from './parse'; -import { replaceUrlHashQuery, replaceUrlQuery } from './format'; +import { parseUrl, parseUrlHash } from '../../../common/state_management/parse'; +import { decodeState } from '../state_encoder'; import { url as urlUtils } from '../../../common'; +import { + createSetStateToKbnUrl, + SetStateToKbnUrlHashOptions, +} from '../../../common/state_management/set_state_to_kbn_url'; +import { persistState } from '../state_hash'; + +export const getCurrentUrl = (history: History) => history.createHref(history.location); /** * Parses a kibana url and retrieves all the states encoded into the URL, @@ -90,28 +96,23 @@ export function getStateFromKbnUrl( * By default due to Kibana legacy reasons assumed that state is stored in a query inside a hash part of the URL: * http://localhost:5601/oxf/app/kibana#/yourApp?_a={STATE} * - * { storeInHashQuery: false } option should be used in you want to store you state in a main query (not in a hash): + * { storeInHashQuery: false } option should be used in you want to store your state in a main query (not in a hash): * http://localhost:5601/oxf/app/kibana?_a={STATE}#/yourApp */ export function setStateToKbnUrl( key: string, state: State, - { useHash = false, storeInHashQuery = true }: { useHash: boolean; storeInHashQuery?: boolean } = { + { useHash = false, storeInHashQuery = true }: SetStateToKbnUrlHashOptions = { useHash: false, storeInHashQuery: true, }, rawUrl = window.location.href -): string { - const replacer = storeInHashQuery ? replaceUrlHashQuery : replaceUrlQuery; - return replacer(rawUrl, (query) => { - const encoded = encodeState(state, useHash); - return { - ...query, - [key]: encoded, - }; - }); +) { + return internalSetStateToKbnUrl(key, state, { useHash, storeInHashQuery }, rawUrl); } +const internalSetStateToKbnUrl = createSetStateToKbnUrl(persistState); + /** * A tiny wrapper around history library to listen for url changes and update url * History library handles a bunch of cross browser edge cases diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 6628bd8a2208..291b20028917 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -23,8 +23,8 @@ import { createStartServicesGetter, Storage, withNotifyOnErrors, - replaceUrlHashQuery, } from '@kbn/kibana-utils-plugin/public'; +import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 983f81090927..b9be3c673448 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -785,7 +785,10 @@ export class DiscoverPageObject extends FtrService { return button.getAttribute('title'); } - public async getCurrentDataViewId() { + /** + * Validates if data view references in the URL are equal. + */ + public async validateDataViewReffsEquality() { const currentUrl = await this.browser.getCurrentUrl(); const matches = currentUrl.matchAll(/index:[^,]*/g); const indexes = []; @@ -798,14 +801,25 @@ export class DiscoverPageObject extends FtrService { if (first) { const allEqual = indexes.every((val) => val === first); if (allEqual) { - return first; + return { valid: true, result: first }; } else { - throw new Error( - 'Discover URL state contains different index references. They should be all the same.' - ); + return { + valid: false, + message: + 'Discover URL state contains different index references. They should be all the same.', + }; } } - throw new Error("Discover URL state doesn't contain an index reference."); + return { valid: false, message: "Discover URL state doesn't contain an index reference." }; + } + + public async getCurrentDataViewId() { + const validationResult = await this.validateDataViewReffsEquality(); + if (validationResult.valid) { + return validationResult.result!; + } else { + throw new Error(validationResult.message); + } } public async addRuntimeField(name: string, script: string) { diff --git a/x-pack/plugins/alerting/kibana.json b/x-pack/plugins/alerting/kibana.json index f4e6a917b19d..9b17c97ae722 100644 --- a/x-pack/plugins/alerting/kibana.json +++ b/x-pack/plugins/alerting/kibana.json @@ -12,6 +12,8 @@ "requiredPlugins": [ "actions", "data", + "dataViews", + "share", "encryptedSavedObjects", "eventLog", "features", diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index aaafa3e32da1..23d68d74c856 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -10,7 +10,9 @@ import { savedObjectsClientMock, uiSettingsServiceMock, } from '@kbn/core/server/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { searchSourceCommonMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { SharePluginStart } from '@kbn/share-plugin/server'; import { rulesClientMock } from './rules_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { Alert, AlertFactoryDoneUtils } from './alert'; @@ -32,6 +34,21 @@ const createSetupMock = () => { return mock; }; +const createShareStartMock = () => { + const startContract = { + url: { + locators: { + get: (id: string) => { + if (id === 'DISCOVER_APP_LOCATOR') { + return { getRedirectUrl: (params: unknown) => JSON.stringify(params) }; + } + }, + }, + }, + } as SharePluginStart; + return startContract; +}; + const createStartMock = () => { const mock: jest.Mocked = { listTypes: jest.fn(), @@ -148,6 +165,8 @@ const createRuleExecutorServicesMock = < search: createAbortableSearchServiceMock(), searchSourceClient: searchSourceCommonMock, ruleMonitoringService: createRuleMonitoringServiceMock(), + share: createShareStartMock(), + dataViews: dataViewPluginMocks.createStartContract(), }; }; export type RuleExecutorServicesMock = ReturnType; diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 87ed23c66ba3..beda3850016d 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -21,8 +21,13 @@ import { eventLogMock } from '@kbn/event-log-plugin/server/mocks'; import { actionsMock } from '@kbn/actions-plugin/server/mocks'; import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; import { monitoringCollectionMock } from '@kbn/monitoring-collection-plugin/server/mocks'; -import { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; +import { + DataViewsServerPluginStart, + PluginSetup as DataPluginSetup, +} from '@kbn/data-plugin/server'; import { spacesMock } from '@kbn/spaces-plugin/server/mocks'; +import { SharePluginStart } from '@kbn/share-plugin/server'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; const generateAlertingConfig = (): AlertingConfig => ({ healthCheck: { @@ -225,6 +230,12 @@ describe('Alerting Plugin', () => { eventLog: eventLogMock.createStart(), taskManager: taskManagerMock.createStart(), data: dataPluginMock.createStartContract(), + share: {} as SharePluginStart, + dataViews: { + dataViewsServiceFactory: jest + .fn() + .mockResolvedValue(dataViewPluginMocks.createStartContract()), + } as DataViewsServerPluginStart, }); expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false); @@ -265,6 +276,12 @@ describe('Alerting Plugin', () => { eventLog: eventLogMock.createStart(), taskManager: taskManagerMock.createStart(), data: dataPluginMock.createStartContract(), + share: {} as SharePluginStart, + dataViews: { + dataViewsServiceFactory: jest + .fn() + .mockResolvedValue(dataViewPluginMocks.createStartContract()), + } as DataViewsServerPluginStart, }); const fakeRequest = { @@ -316,6 +333,12 @@ describe('Alerting Plugin', () => { eventLog: eventLogMock.createStart(), taskManager: taskManagerMock.createStart(), data: dataPluginMock.createStartContract(), + share: {} as SharePluginStart, + dataViews: { + dataViewsServiceFactory: jest + .fn() + .mockResolvedValue(dataViewPluginMocks.createStartContract()), + } as DataViewsServerPluginStart, }); const fakeRequest = { diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 48d1bfee78e4..13f39504d278 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -11,6 +11,7 @@ import { pick } from 'lodash'; import { UsageCollectionSetup, UsageCounter } from '@kbn/usage-collection-plugin/server'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; import { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; +import { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin/server'; import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart, @@ -49,6 +50,7 @@ import { import { PluginStartContract as FeaturesPluginStart } from '@kbn/features-plugin/server'; import { PluginStart as DataPluginStart } from '@kbn/data-plugin/server'; import { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server'; +import { SharePluginStart } from '@kbn/share-plugin/server'; import { RuleTypeRegistry } from './rule_type_registry'; import { TaskRunnerFactory } from './task_runner'; import { RulesClientFactory } from './rules_client_factory'; @@ -156,6 +158,8 @@ export interface AlertingPluginsStart { spaces: SpacesPluginStart; security?: SecurityPluginStart; data: DataPluginStart; + dataViews: DataViewsPluginStart; + share: SharePluginStart; } export class AlertingPlugin { @@ -428,6 +432,8 @@ export class AlertingPlugin { taskRunnerFactory.initialize({ logger, data: plugins.data, + share: plugins.share, + dataViews: plugins.dataViews, savedObjects: core.savedObjects, uiSettings: core.uiSettings, elasticsearch: core.elasticsearch, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index c39c6200952a..1dbb3b4a5561 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -73,6 +73,9 @@ import { RuleContextOpts, } from '../lib/alerting_event_logger/alerting_event_logger'; import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock'; +import { SharePluginStart } from '@kbn/share-plugin/server'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -121,6 +124,9 @@ describe('Task Runner', () => { const dataPlugin = dataPluginMock.createStartContract(); const uiSettingsService = uiSettingsServiceMock.createStartContract(); const inMemoryMetrics = inMemoryMetricsMock.create(); + const dataViewsMock = { + dataViewsServiceFactory: jest.fn().mockResolvedValue(dataViewPluginMocks.createStartContract()), + } as DataViewsServerPluginStart; type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; @@ -130,7 +136,9 @@ describe('Task Runner', () => { const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { data: dataPlugin, + dataViews: dataViewsMock, savedObjects: savedObjectsService, + share: {} as SharePluginStart, uiSettings: uiSettingsService, elasticsearch: elasticsearchService, actionsPlugin: actionsMock.createStart(), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 785592190c63..426ae7b2554e 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -325,6 +325,11 @@ export class TaskRunner< includedHiddenTypes: ['alert', 'action'], }); + const dataViews = await this.context.dataViews.dataViewsServiceFactory( + savedObjectsClient, + scopedClusterClient.asInternalUser + ); + updatedState = await this.context.executionContext.withContext(ctx, () => this.ruleType.executor({ executionId: this.executionId, @@ -337,6 +342,8 @@ export class TaskRunner< shouldWriteAlerts: () => this.shouldLogAndScheduleActionsForAlerts(), shouldStopExecution: () => this.cancelled, ruleMonitoringService: this.ruleMonitoring.getLastRunMetricsSetters(), + dataViews, + share: this.context.share, ruleResultService: this.ruleResult.getLastRunSetters(), }, params, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 49a21d62d302..b21a8852a701 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -50,6 +50,9 @@ import { generateActionOpts, } from './fixtures'; import { EVENT_LOG_ACTIONS } from '../plugin'; +import { SharePluginStart } from '@kbn/share-plugin/server'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -66,6 +69,9 @@ const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const alertingEventLogger = alertingEventLoggerMock.create(); const logger: ReturnType = loggingSystemMock.createLogger(); +const dataViewsMock = { + dataViewsServiceFactory: jest.fn().mockResolvedValue(dataViewPluginMocks.createStartContract()), +} as DataViewsServerPluginStart; describe('Task Runner Cancel', () => { let mockedTaskInstance: ConcreteTaskInstance; @@ -106,7 +112,9 @@ describe('Task Runner Cancel', () => { const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { data: dataPlugin, + dataViews: dataViewsMock, savedObjects: savedObjectsService, + share: {} as SharePluginStart, uiSettings: uiSettingsService, elasticsearch: elasticsearchService, actionsPlugin: actionsMock.createStart(), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 9ecc361ca4af..9864a6caafc8 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -26,6 +26,9 @@ import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { executionContextServiceMock } from '@kbn/core/server/mocks'; import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; +import { SharePluginStart } from '@kbn/share-plugin/server'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; const inMemoryMetrics = inMemoryMetricsMock.create(); const executionContext = executionContextServiceMock.createSetupContract(); @@ -35,6 +38,9 @@ const savedObjectsService = savedObjectsServiceMock.createInternalStartContract( const uiSettingsService = uiSettingsServiceMock.createStartContract(); const elasticsearchService = elasticsearchServiceMock.createInternalStart(); const dataPlugin = dataPluginMock.createStartContract(); +const dataViewsMock = { + dataViewsServiceFactory: jest.fn().mockResolvedValue(dataViewPluginMocks.createStartContract()), +} as DataViewsServerPluginStart; const ruleType: UntypedNormalizedRuleType = { id: 'test', name: 'My test alert', @@ -83,7 +89,9 @@ describe('Task Runner Factory', () => { const taskRunnerFactoryInitializerParams: jest.Mocked = { data: dataPlugin, + dataViews: dataViewsMock, savedObjects: savedObjectsService, + share: {} as SharePluginStart, uiSettings: uiSettingsService, elasticsearch: elasticsearchService, getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts index a5ca6973cda2..c323a87d4522 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -16,11 +16,13 @@ import type { ElasticsearchServiceStart, UiSettingsServiceStart, } from '@kbn/core/server'; +import { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin/server'; import { RunContext } from '@kbn/task-manager-plugin/server'; import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; import { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server'; import { IEventLogger } from '@kbn/event-log-plugin/server'; import { PluginStart as DataPluginStart } from '@kbn/data-plugin/server'; +import { SharePluginStart } from '@kbn/share-plugin/server'; import { RuleTypeParams, RuleTypeRegistry, @@ -38,6 +40,8 @@ import { ActionsConfigMap } from '../lib/get_actions_config_map'; export interface TaskRunnerContext { logger: Logger; data: DataPluginStart; + dataViews: DataViewsPluginStart; + share: SharePluginStart; savedObjects: SavedObjectsServiceStart; uiSettings: UiSettingsServiceStart; elasticsearch: ElasticsearchServiceStart; diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 5bdf65e4c564..b1b55066836c 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -11,6 +11,7 @@ import type { SavedObjectReference, IUiSettingsClient, } from '@kbn/core/server'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { ISearchStartSearchSource } from '@kbn/data-plugin/common'; import { LicenseType } from '@kbn/licensing-plugin/server'; import { @@ -20,6 +21,7 @@ import { Logger, } from '@kbn/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; +import { SharePluginStart } from '@kbn/share-plugin/server'; import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { RulesClient } from './rules_client'; @@ -84,6 +86,8 @@ export interface RuleExecutorServices< shouldWriteAlerts: () => boolean; shouldStopExecution: () => boolean; ruleMonitoringService?: PublicRuleMonitoringService; + share: SharePluginStart; + dataViews: DataViewsContract; ruleResultService?: PublicRuleResultService; } diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index 9ea5bf8dcb94..0d59ff6ac786 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -36,6 +36,8 @@ "@kbn/core-logging-server-mocks", "@kbn/core-saved-objects-common", "@kbn/securitysolution-rules", + "@kbn/data-views-plugin", + "@kbn/share-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts index 5ac612c8c5fd..98878b2264da 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -15,7 +15,7 @@ import { VISUALIZE_EMBEDDABLE_TYPE, } from '@kbn/visualizations-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; -import { DiscoverAppLocator } from '@kbn/discover-plugin/public'; +import { DiscoverAppLocator } from '@kbn/discover-plugin/common'; import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; const i18nTranslateSpy = i18n.translate as unknown as jest.SpyInstance; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index f3f8512b8525..df1d21b3a727 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -6,7 +6,8 @@ */ import { Action } from '@kbn/ui-actions-plugin/public'; -import { DiscoverAppLocatorParams, SearchInput } from '@kbn/discover-plugin/public'; +import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; +import { SearchInput } from '@kbn/discover-plugin/public'; import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public'; import { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { KibanaLocation } from '@kbn/share-plugin/public'; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts index 974f4126778f..0e56a7edc735 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts @@ -14,7 +14,7 @@ import { VISUALIZE_EMBEDDABLE_TYPE, } from '@kbn/visualizations-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; -import { DiscoverAppLocator } from '@kbn/discover-plugin/public'; +import { DiscoverAppLocator } from '@kbn/discover-plugin/common'; import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; const i18nTranslateSpy = i18n.translate as unknown as jest.SpyInstance; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts index f3e8fa6c393a..2329ac6c7071 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts @@ -8,7 +8,7 @@ import type { Filter } from '@kbn/es-query'; import { Action } from '@kbn/ui-actions-plugin/public'; import { EmbeddableContext, EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public'; import type { Query, TimeRange } from '@kbn/es-query'; -import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/public'; +import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { KibanaLocation } from '@kbn/share-plugin/public'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts index 4dbed536767e..dd6637d86960 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts @@ -27,12 +27,14 @@ import { import { FIRED_ACTION, getRuleExecutor } from './executor'; import { aStoredSLO, createSLO } from '../../../services/slo/fixtures/slo'; import { SLO } from '../../../domain/models'; +import { SharePluginStart } from '@kbn/share-plugin/server'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { - AlertStates, - BurnRateAlertContext, BurnRateAlertState, + BurnRateAlertContext, BurnRateAllowedActionGroups, BurnRateRuleParams, + AlertStates, } from './types'; const commonEsResponse = { @@ -92,6 +94,8 @@ describe('BurnRateRuleExecutor', () => { getAlertStartedDate: jest.fn(), getAlertUuid: jest.fn(), getAlertByAlertUuid: jest.fn(), + share: {} as SharePluginStart, + dataViews: dataViewPluginMocks.createStartContract(), }; }); diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index ee5ea65e9c79..77b91efa88ee 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -20,6 +20,8 @@ import { RuleDataClient } from '../rule_data_client'; import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock'; import { createLifecycleRuleTypeFactory } from './create_lifecycle_rule_type_factory'; import { ISearchStartSearchSource } from '@kbn/data-plugin/common'; +import { SharePluginStart } from '@kbn/share-plugin/server'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; type RuleTestHelpers = ReturnType; @@ -129,6 +131,8 @@ function createRule(shouldWriteAlerts: boolean = true) { shouldStopExecution: () => false, shouldWriteAlerts: () => shouldWriteAlerts, uiSettingsClient: {} as any, + share: {} as SharePluginStart, + dataViews: dataViewPluginMocks.createStartContract(), }, spaceId: 'spaceId', startedAt, diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts index 70c827942ade..9e75fa159190 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts @@ -19,6 +19,8 @@ import { import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; import { searchSourceCommonMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { Logger } from '@kbn/logging'; +import { SharePluginStart } from '@kbn/share-plugin/server'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; export const createDefaultAlertExecutorOptions = < Params extends RuleTypeParams = never, @@ -77,6 +79,8 @@ export const createDefaultAlertExecutorOptions = < shouldWriteAlerts: () => shouldWriteAlerts, shouldStopExecution: () => false, searchSourceClient: searchSourceCommonMock, + share: {} as SharePluginStart, + dataViews: dataViewPluginMocks.createStartContract(), }, state, previousStartedAt: null, diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index 7b282319d62d..a3a2a6d373b2 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -31,6 +31,7 @@ "@kbn/logging-mocks", "@kbn/logging", "@kbn/securitysolution-io-ts-utils", + "@kbn/share-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 854ece9ac26e..14dea5909c8d 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -16,6 +16,7 @@ "cloudSecurityPosture", "dashboard", "data", + "dataViews", "embeddable", "eventLog", "features", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index 5dbc62c86c41..9efb64c9582a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -91,7 +91,7 @@ export const previewRulesRoute = async ( return siemResponse.error({ statusCode: 400, body: validationErrors }); } try { - const [, { data, security: securityService }] = await getStartServices(); + const [, { data, security: securityService, share, dataViews }] = await getStartServices(); const searchSourceClient = await data.search.searchSource.asScoped(request); const savedObjectsClient = coreContext.savedObjects.client; const siemClient = (await context.securitySolution).getAppClient(); @@ -229,6 +229,11 @@ export const previewRulesRoute = async ( let invocationStartTime; + const dataViewsService = await dataViews.dataViewsServiceFactory( + savedObjectsClient, + coreContext.elasticsearch.client.asInternalUser + ); + while (invocationCount > 0 && !isAborted) { invocationStartTime = moment(); @@ -251,6 +256,8 @@ export const previewRulesRoute = async ( searchSourceClient, }), uiSettingsClient: coreContext.uiSettings.client, + dataViews: dataViewsService, + share, }, spaceId, startedAt: startedAt.toDate(), diff --git a/x-pack/plugins/security_solution/server/plugin_contract.ts b/x-pack/plugins/security_solution/server/plugin_contract.ts index 30accf442825..fffb019e5eaf 100644 --- a/x-pack/plugins/security_solution/server/plugin_contract.ts +++ b/x-pack/plugins/security_solution/server/plugin_contract.ts @@ -10,6 +10,7 @@ import type { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, } from '@kbn/data-plugin/server'; +import type { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin/server'; import type { UsageCollectionSetup as UsageCollectionPluginSetup } from '@kbn/usage-collection-plugin/server'; import type { PluginSetupContract as AlertingPluginSetup, @@ -37,6 +38,7 @@ import type { TelemetryPluginStart, TelemetryPluginSetup } from '@kbn/telemetry- import type { OsqueryPluginSetup } from '@kbn/osquery-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common'; +import type { SharePluginStart } from '@kbn/share-plugin/server'; import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified-search-plugin/server'; @@ -66,6 +68,7 @@ export interface SecuritySolutionPluginStartDependencies { cases?: CasesPluginStart; cloudExperiments?: CloudExperimentsPluginStart; data: DataPluginStart; + dataViews: DataViewsPluginStart; eventLog: IEventLogClientService; fleet?: FleetPluginStart; licensing: LicensingPluginStart; @@ -74,6 +77,7 @@ export interface SecuritySolutionPluginStartDependencies { spaces?: SpacesPluginStart; taskManager?: TaskManagerPluginStart; telemetry?: TelemetryPluginStart; + share: SharePluginStart; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap index 0f1db835a828..8036c63e4f48 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap @@ -25,6 +25,7 @@ exports[`should render BoundaryIndexExpression 1`] = ` indexPatternService={ Object { "clearCache": [MockFunction], + "create": [MockFunction], "createField": [MockFunction], "createFieldList": [MockFunction], "ensureDefaultDataView": [MockFunction], @@ -112,6 +113,7 @@ exports[`should render EntityIndexExpression 1`] = ` indexPatternService={ Object { "clearCache": [MockFunction], + "create": [MockFunction], "createField": [MockFunction], "createFieldList": [MockFunction], "ensureDefaultDataView": [MockFunction], @@ -205,6 +207,7 @@ exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = ` indexPatternService={ Object { "clearCache": [MockFunction], + "create": [MockFunction], "createField": [MockFunction], "createFieldList": [MockFunction], "ensureDefaultDataView": [MockFunction], diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts index 9444657a36eb..b42623d91cab 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts @@ -144,6 +144,8 @@ describe('es_query executor', () => { name: 'test-rule-name', alertLimit: 1000, params: defaultProps, + publicBaseUrl: 'https://localhost:5601', + spacePrefix: '', timestamp: undefined, services: { scopedClusterClient: scopedClusterClientMock, @@ -180,7 +182,9 @@ describe('es_query executor', () => { services: { searchSourceClient: searchSourceClientMock, logger, + share: undefined, }, + spacePrefix: '', }); expect(mockFetchEsQuery).not.toHaveBeenCalled(); }); @@ -225,6 +229,7 @@ describe('es_query executor', () => { }, dateStart: new Date().toISOString(), dateEnd: new Date().toISOString(), + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', }); await executor(coreMock, { ...defaultExecutorOptions, @@ -277,6 +282,7 @@ describe('es_query executor', () => { }, dateStart: new Date().toISOString(), dateEnd: new Date().toISOString(), + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', }); await executor(coreMock, { ...defaultExecutorOptions, @@ -413,6 +419,7 @@ describe('es_query executor', () => { }, dateStart: new Date().toISOString(), dateEnd: new Date().toISOString(), + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', }); await executor(coreMock, { ...defaultExecutorOptions, @@ -456,6 +463,7 @@ describe('es_query executor', () => { parsedResults: { results: [], truncated: false }, dateStart: new Date().toISOString(), dateEnd: new Date().toISOString(), + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', }); await executor(coreMock, { ...defaultExecutorOptions, diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts index ac179eb144d7..6a690e745bdc 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts @@ -32,9 +32,10 @@ export async function executor(core: CoreSetup, options: ExecutorOptions = {}; for (const result of parsedResults.results) { const alertId = result.group; diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts index ef32196f02fa..10e8278f1976 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts @@ -63,6 +63,8 @@ describe('fetchEsQuery', () => { params, timestamp: '2020-02-09T23:15:41.941Z', services, + spacePrefix: '', + publicBaseUrl: '', }); expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith( { @@ -151,6 +153,8 @@ describe('fetchEsQuery', () => { params, timestamp: undefined, services, + spacePrefix: '', + publicBaseUrl: '', }); expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith( { @@ -213,6 +217,8 @@ describe('fetchEsQuery', () => { params, timestamp: '2020-02-09T23:15:41.941Z', services, + spacePrefix: '', + publicBaseUrl: '', }); expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith( { @@ -275,6 +281,8 @@ describe('fetchEsQuery', () => { params, timestamp: undefined, services, + spacePrefix: '', + publicBaseUrl: '', }); expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith( { diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts index b44b45ed108b..8c44c6e673ad 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts @@ -23,6 +23,8 @@ export interface FetchEsQueryOpts { name: string; params: OnlyEsQueryRuleParams; timestamp: string | undefined; + publicBaseUrl: string; + spacePrefix: string; services: { scopedClusterClient: IScopedClusterClient; logger: Logger; @@ -37,6 +39,8 @@ export async function fetchEsQuery({ ruleId, name, params, + spacePrefix, + publicBaseUrl, timestamp, services, alertLimit, @@ -123,6 +127,8 @@ export async function fetchEsQuery({ ` es query rule ${ES_QUERY_ID}:${ruleId} "${name}" result - ${JSON.stringify(searchResult)}` ); + const link = `${publicBaseUrl}${spacePrefix}/app/management/insightsAndAlerting/triggersActions/rule/${ruleId}`; + return { parsedResults: parseAggregationResults({ isCountAgg, @@ -132,5 +138,6 @@ export async function fetchEsQuery({ }), dateStart, dateEnd, + link, }; } diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts index 196cb5d709fd..9a4026b58238 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts @@ -64,6 +64,7 @@ describe('fetchSearchSourceQuery', () => { const { searchSource, dateStart, dateEnd } = updateSearchSource( searchSourceInstance, + dataViewMock, params, undefined ); @@ -101,6 +102,7 @@ describe('fetchSearchSourceQuery', () => { const { searchSource } = updateSearchSource( searchSourceInstance, + dataViewMock, params, '2020-02-09T23:12:41.941Z' ); @@ -143,6 +145,7 @@ describe('fetchSearchSourceQuery', () => { const { searchSource } = updateSearchSource( searchSourceInstance, + dataViewMock, params, '2020-01-09T22:12:41.941Z' ); @@ -178,6 +181,7 @@ describe('fetchSearchSourceQuery', () => { const { searchSource } = updateSearchSource( searchSourceInstance, + dataViewMock, params, '2020-02-09T23:12:41.941Z' ); @@ -219,6 +223,7 @@ describe('fetchSearchSourceQuery', () => { const { searchSource } = updateSearchSource( searchSourceInstance, + dataViewMock, params, '2020-02-09T23:12:41.941Z' ); diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts index 172232e0067b..ac9df7a0c0f8 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts @@ -4,9 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { buildRangeFilter, Filter } from '@kbn/es-query'; -import { Logger } from '@kbn/core/server'; import { + DataView, + DataViewsContract, getTime, ISearchSource, ISearchStartSearchSource, @@ -19,26 +21,34 @@ import { parseAggregationResults, } from '@kbn/triggers-actions-ui-plugin/common'; import { isGroupAggregation } from '@kbn/triggers-actions-ui-plugin/common'; +import { SharePluginStart } from '@kbn/share-plugin/server'; +import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; +import { Logger } from '@kbn/core/server'; +import { LocatorPublic } from '@kbn/share-plugin/common'; import { OnlySearchSourceRuleParams } from '../types'; import { getComparatorScript } from '../../../../common'; export interface FetchSearchSourceQueryOpts { ruleId: string; + alertLimit: number | undefined; params: OnlySearchSourceRuleParams; latestTimestamp: string | undefined; + spacePrefix: string; services: { - searchSourceClient: ISearchStartSearchSource; logger: Logger; + searchSourceClient: ISearchStartSearchSource; + share: SharePluginStart; + dataViews: DataViewsContract; }; - alertLimit?: number; } export async function fetchSearchSourceQuery({ ruleId, + alertLimit, params, latestTimestamp, + spacePrefix, services, - alertLimit, }: FetchSearchSourceQueryOpts) { const { logger, searchSourceClient } = services; const isGroupAgg = isGroupAggregation(params.termField); @@ -46,8 +56,10 @@ export async function fetchSearchSourceQuery({ const initialSearchSource = await searchSourceClient.create(params.searchConfiguration); + const index = initialSearchSource.getField('index') as DataView; const { searchSource, dateStart, dateEnd } = updateSearchSource( initialSearchSource, + index, params, latestTimestamp, alertLimit @@ -61,7 +73,19 @@ export async function fetchSearchSourceQuery({ const searchResult = await searchSource.fetch(); + const link = await generateLink( + initialSearchSource, + services.share.url.locators.get('DISCOVER_APP_LOCATOR')!, + services.dataViews, + index, + dateStart, + dateEnd, + spacePrefix + ); return { + link, + numMatches: Number(searchResult.hits.total), + searchResult, parsedResults: parseAggregationResults({ isCountAgg, isGroupAgg, esResult: searchResult }), dateStart, dateEnd, @@ -70,12 +94,12 @@ export async function fetchSearchSourceQuery({ export function updateSearchSource( searchSource: ISearchSource, + index: DataView, params: OnlySearchSourceRuleParams, latestTimestamp: string | undefined, alertLimit?: number ) { const isGroupAgg = isGroupAggregation(params.termField); - const index = searchSource.getField('index')!; const timeFieldName = params.timeField || index.timeFieldName; if (!timeFieldName) { @@ -84,10 +108,11 @@ export function updateSearchSource( searchSource.setField('size', isGroupAgg ? 0 : params.size); - const timerangeFilter = getTime(index, { + const timeRange = { from: `now-${params.timeWindowSize}${params.timeWindowUnit}`, to: 'now', - }); + }; + const timerangeFilter = getTime(index, timeRange); const dateStart = timerangeFilter?.query.range[timeFieldName].gte; const dateEnd = timerangeFilter?.query.range[timeFieldName].lte; const filters = [timerangeFilter]; @@ -129,3 +154,51 @@ export function updateSearchSource( dateEnd, }; } + +async function generateLink( + searchSource: ISearchSource, + discoverLocator: LocatorPublic, + dataViews: DataViewsContract, + dataViewToUpdate: DataView, + dateStart: string, + dateEnd: string, + spacePrefix: string +) { + const prevFilters = searchSource.getField('filter') as Filter[]; + + // make new adhoc data view + const newDataView = await dataViews.create({ + ...dataViewToUpdate.toSpec(false), + version: undefined, + id: undefined, + }); + const updatedFilters = updateFilterReferences(prevFilters, dataViewToUpdate.id!, newDataView.id!); + + const redirectUrlParams: DiscoverAppLocatorParams = { + dataViewSpec: newDataView.toSpec(false), + filters: updatedFilters, + query: searchSource.getField('query'), + timeRange: { from: dateStart, to: dateEnd }, + isAlertResults: true, + }; + const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams); + const [start, end] = redirectUrl.split('/app'); + + return start + spacePrefix + '/app' + end; +} + +function updateFilterReferences(filters: Filter[], fromDataView: string, toDataView: string) { + return filters.map((filter) => { + if (filter.meta.index === fromDataView) { + return { + ...filter, + meta: { + ...filter.meta, + index: toDataView, + }, + }; + } else { + return filter; + } + }); +} diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts index d624c5a8dc3c..bb27619c1e18 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts @@ -518,6 +518,9 @@ describe('ruleType', () => { aggregatable: false, }, ], + toSpec: () => { + return { id: 'test-id', title: 'test-title', timeFieldName: 'time-field' }; + }, }; const defaultParams: OnlySearchSourceRuleParams = { size: 100, @@ -564,10 +567,16 @@ describe('ruleType', () => { const searchResult: ESSearchResponse = generateResults([]); const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); - (searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => { + (ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({ + toSpec: () => dataViewMock.toSpec(), + }); + (searchSourceInstanceMock.getField as jest.Mock).mockImplementation((name: string) => { if (name === 'index') { return dataViewMock; } + if (name === 'filter') { + return []; + } }); (searchSourceInstanceMock.fetch as jest.Mock).mockResolvedValueOnce(searchResult); @@ -595,10 +604,16 @@ describe('ruleType', () => { const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] }; const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); - (searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => { + (ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({ + toSpec: () => dataViewMock.toSpec(), + }); + (searchSourceInstanceMock.getField as jest.Mock).mockImplementation((name: string) => { if (name === 'index') { return dataViewMock; } + if (name === 'filter') { + return []; + } }); (searchSourceInstanceMock.fetch as jest.Mock).mockResolvedValueOnce({ diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index 444a05797cec..81e9d5b57bcf 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -40,6 +40,8 @@ "@kbn/react-field", "@kbn/core-elasticsearch-server-mocks", "@kbn/logging-mocks", + "@kbn/share-plugin", + "@kbn/discover-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index c646aaeadad0..4507dcc70a62 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -26,7 +26,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/public'; +import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common'; import { DuplicateDataViewError } from '@kbn/data-plugin/public'; import type { RuntimeField } from '@kbn/data-views-plugin/common'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/use_action_discover.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/use_action_discover.tsx index aeb5c3e2d09a..24ee1f9dfe09 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/use_action_discover.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/use_action_discover.tsx @@ -7,8 +7,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/public'; - +import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common'; import { TransformListAction, TransformListRow } from '../../../../common'; import { useSearchItems } from '../../../../hooks/use_search_items'; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c9105d5c018d..40b8dcd9b6f7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2430,14 +2430,8 @@ "discover.uninitializedRefreshButtonText": "Actualiser les données", "discover.uninitializedText": "Saisissez une requête, ajoutez quelques filtres, ou cliquez simplement sur Actualiser afin d’extraire les résultats pour la requête en cours.", "discover.uninitializedTitle": "Commencer la recherche", - "discover.viewAlert.alertRuleChangedWarnDescription": "Les documents affichés peuvent ne pas correspondre à ceux ayant déclenché l'alerte\n car la configuration de la règle a été modifiée.", - "discover.viewAlert.alertRuleChangedWarnTitle": "La règle d'alerte a été modifiée", "discover.viewAlert.alertRuleFetchErrorTitle": "Erreur lors de la récupération de la règle d'alerte", - "discover.viewAlert.dataViewChangedWarnDescription": "La vue de données a été mise à jour après la dernière mise à jour de la règle d'alerte.", - "discover.viewAlert.dataViewChangedWarnTitle": "La vue de données a changé", "discover.viewAlert.dataViewErrorTitle": "Erreur lors de la récupération de la vue de données", - "discover.viewAlert.documentsMayVaryInfoDescription": "Les documents affichés peuvent différer de ceux ayant déclenché l'alerte.\n Des documents ont peut-être été ajoutés ou supprimés.", - "discover.viewAlert.documentsMayVaryInfoTitle": "Les documents affichés peuvent varier", "discover.viewAlert.searchSourceErrorTitle": "Erreur lors de la récupération de la source de recherche", "discover.viewModes.document.label": "Documents", "discover.viewModes.fieldStatistics.label": "Statistiques de champ", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3f40f72ea244..09d95547cee9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2428,14 +2428,8 @@ "discover.uninitializedRefreshButtonText": "データを更新", "discover.uninitializedText": "クエリを作成、フィルターを追加、または[更新]をクリックして、現在のクエリの結果を取得します。", "discover.uninitializedTitle": "検索開始", - "discover.viewAlert.alertRuleChangedWarnDescription": "表示されたドキュメントは、アラートをトリガーしたドキュメントと一致しない場合があります。\n これはルール構成が変更されたためです。", - "discover.viewAlert.alertRuleChangedWarnTitle": "アラートルールが変更されました", "discover.viewAlert.alertRuleFetchErrorTitle": "アラートルールの取り込みエラー", - "discover.viewAlert.dataViewChangedWarnDescription": "アラートルールの最後の更新の後に、データビューが更新されました。", - "discover.viewAlert.dataViewChangedWarnTitle": "データビューが変更されました", "discover.viewAlert.dataViewErrorTitle": "データビューの取得エラー", - "discover.viewAlert.documentsMayVaryInfoDescription": "表示されたドキュメントは、アラートをトリガーしたドキュメントとは異なる場合があります。\n 一部のドキュメントが追加または削除された可能性があります。", - "discover.viewAlert.documentsMayVaryInfoTitle": "表示されたドキュメントは異なる場合があります", "discover.viewAlert.searchSourceErrorTitle": "検索ソースの取得エラー", "discover.viewModes.document.label": "ドキュメント", "discover.viewModes.fieldStatistics.label": "フィールド統計情報", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 72dc1fac9c56..6f62334d0d45 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2432,14 +2432,8 @@ "discover.uninitializedRefreshButtonText": "刷新数据", "discover.uninitializedText": "编写查询,添加一些筛选,或只需单击“刷新”来检索当前查询的结果。", "discover.uninitializedTitle": "开始搜索", - "discover.viewAlert.alertRuleChangedWarnDescription": "显示的文档可能与触发告警的文档不匹配,\n 因为规则配置已更改。", - "discover.viewAlert.alertRuleChangedWarnTitle": "告警规则已更改", "discover.viewAlert.alertRuleFetchErrorTitle": "提取告警值时出错", - "discover.viewAlert.dataViewChangedWarnDescription": "已在上次更新告警规则之后更新数据视图。", - "discover.viewAlert.dataViewChangedWarnTitle": "数据视图已更改", "discover.viewAlert.dataViewErrorTitle": "提取数据视图时出错", - "discover.viewAlert.documentsMayVaryInfoDescription": "显示的文档可能与触发告警的文档不同。\n 可能已添加或删除了某些文档。", - "discover.viewAlert.documentsMayVaryInfoTitle": "显示的文档可能有所不同", "discover.viewAlert.searchSourceErrorTitle": "提取搜索源时出错", "discover.viewModes.document.label": "文档", "discover.viewModes.fieldStatistics.label": "字段统计信息", diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index c059530cc019..44aec88719b5 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -33,18 +33,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const toasts = getService('toasts'); - const SOURCE_DATA_INDEX = 'search-source-alert'; - const OUTPUT_DATA_INDEX = 'search-source-alert-output'; + const SOURCE_DATA_VIEW = 'search-source-alert'; + const OUTPUT_DATA_VIEW = 'search-source-alert-output'; const ACTION_TYPE_ID = '.index'; const RULE_NAME = 'test-search-source-alert'; + const ADHOC_RULE_NAME = 'test-adhoc-alert'; let sourceDataViewId: string; - let sourceAdHocDataViewId: string; let outputDataViewId: string; let connectorId: string; const createSourceIndex = () => es.index({ - index: SOURCE_DATA_INDEX, + index: SOURCE_DATA_VIEW, body: { settings: { number_of_shards: 1 }, mappings: { @@ -58,13 +58,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const generateNewDocs = async (docsNumber: number) => { const mockMessages = Array.from({ length: docsNumber }, (_, i) => `msg-${i}`); - const dateNow = new Date().toISOString(); + const dateNow = new Date(); + const dateToSet = new Date(dateNow); + dateToSet.setMinutes(dateNow.getMinutes() - 10); for await (const message of mockMessages) { es.transport.request({ - path: `/${SOURCE_DATA_INDEX}/_doc`, + path: `/${SOURCE_DATA_VIEW}/_doc`, method: 'POST', body: { - '@timestamp': dateNow, + '@timestamp': dateToSet.toISOString(), message, }, }); @@ -73,7 +75,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const createOutputDataIndex = () => es.index({ - index: OUTPUT_DATA_INDEX, + index: OUTPUT_DATA_VIEW, body: { settings: { number_of_shards: 1, @@ -139,7 +141,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { .send({ name: 'search-source-alert-test-connector', connector_type_id: ACTION_TYPE_ID, - config: { index: OUTPUT_DATA_INDEX }, + config: { index: OUTPUT_DATA_VIEW }, secrets: {}, }) .expect(200); @@ -157,7 +159,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return ruleName === alertName; }); await testSubjects.click('thresholdPopover'); - await testSubjects.setValue('alertThresholdInput', '3'); + await testSubjects.setValue('alertThresholdInput', '1'); + + await testSubjects.click('forLastExpression'); + await testSubjects.setValue('timeWindowSizeNumber', '30'); + await retry.waitFor('actions accordion to exist', async () => { await testSubjects.click('.index-alerting-ActionTypeSelectOption'); return await testSubjects.exists('alertActionAccordion-0'); @@ -200,14 +206,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return link; }; - const openAlertResults = async (ruleName: string, dataViewId?: string) => { + const openAlertResults = async (value: string, type: 'id' | 'name' = 'name') => { await PageObjects.common.navigateToApp('discover'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.clickNewSearchButton(); // reset params - await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_INDEX); + await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_VIEW); + + let alertId: string; + if (type === 'name') { + const [{ id }] = await getAlertsByName(value); + alertId = id; + } else { + alertId = value; + } - const [{ id: alertId }] = await getAlertsByName(ruleName); await filterBar.addFilter({ field: 'alert_id', operation: 'is', value: alertId }); await PageObjects.discover.waitUntilSearchingHasFinished(); @@ -218,11 +231,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const baseUrl = deployment.getHostPort(); await browser.navigateTo(baseUrl + link); await PageObjects.discover.waitUntilSearchingHasFinished(); - - await retry.waitFor('navigate to discover', async () => { - const currentDataViewId = await PageObjects.discover.getCurrentDataViewId(); - return dataViewId ? currentDataViewId === dataViewId : true; - }); }; const openAlertRuleInManagement = async (ruleName: string) => { @@ -238,6 +246,66 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); }; + const clickViewInApp = async (ruleName: string) => { + // navigate to discover using view in app link + await openAlertRuleInManagement(ruleName); + await testSubjects.click('ruleDetails-viewInApp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + const checkInitialRuleParamsState = async (dataView: string, isViewInApp = false) => { + if (isViewInApp) { + expect(await toasts.getToastCount()).to.be(0); + } else { + expect(await toasts.getToastCount()).to.be(1); + expect(await toasts.getToastContent(1)).to.equal( + `Displayed documents may vary\nThe displayed documents might differ from the documents that triggered the alert. Some documents might have been added or deleted.` + ); + } + expect(await filterBar.getFilterCount()).to.be(0); + expect(await queryBar.getQueryString()).to.equal(''); + const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView(); + const { valid } = await PageObjects.discover.validateDataViewReffsEquality(); + expect(valid).to.equal(true); + expect(selectedDataView).to.be.equal(dataView); + expect(await dataGrid.getDocCount()).to.be(5); + }; + + const checkUpdatedRuleParamsState = async () => { + expect(await toasts.getToastCount()).to.be(0); + const queryString = await queryBar.getQueryString(); + const hasFilter = await filterBar.hasFilter('message.keyword', 'msg-1'); + expect(queryString).to.be.equal('message:msg-1'); + expect(hasFilter).to.be.equal(true); + expect(await dataGrid.getDocCount()).to.be(1); + }; + + const checkInitialDataViewState = async (dataView: string) => { + // validate prev field filter + await testSubjects.existOrFail(`field-message-showDetails`); // still exists + + // validate prev title + await PageObjects.discover.clickIndexPatternActions(); + await testSubjects.click('indexPattern-manage-field'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const titleElem = await testSubjects.find('currentIndexPatternTitle'); + expect(await titleElem.getVisibleText()).to.equal(dataView); + }; + + const checkUpdatedDataViewState = async (dataView: string) => { + // validate updated field filter + await testSubjects.missingOrFail(`field-message-showDetails`); + + // validate updated title + await PageObjects.discover.clickIndexPatternActions(); + await testSubjects.click('indexPattern-manage-field'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const titleElem = await testSubjects.find('currentIndexPatternTitle'); + expect(await titleElem.getVisibleText()).to.equal(dataView); + }; + describe('Search source Alert', () => { before(async () => { await security.testUser.setRoles(['discover_alert']); @@ -256,11 +324,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { - deleteIndexes([OUTPUT_DATA_INDEX, SOURCE_DATA_INDEX]); + deleteIndexes([OUTPUT_DATA_VIEW, SOURCE_DATA_VIEW]); + const [{ id: adhocRuleId }] = await getAlertsByName(ADHOC_RULE_NAME); + await deleteAlerts([adhocRuleId]); await deleteDataView(outputDataViewId); await deleteConnector(connectorId); - const alertsToDelete = await getAlertsByName('test'); - await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); await security.testUser.restoreDefaults(); }); @@ -272,8 +340,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await dataViewSelector.getVisibleText()).to.eql('DATA VIEW\nSelect a data view'); log.debug('create data views'); - const sourceDataViewResponse = await createDataView(SOURCE_DATA_INDEX); - const outputDataViewResponse = await createDataView(OUTPUT_DATA_INDEX); + const sourceDataViewResponse = await createDataView(SOURCE_DATA_VIEW); + const outputDataViewResponse = await createDataView(OUTPUT_DATA_VIEW); sourceDataViewId = sourceDataViewResponse.body.data_view.id; outputDataViewId = outputDataViewResponse.body.data_view.id; @@ -282,7 +350,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show time field validation error', async () => { await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.waitUntilSearchingHasFinished(); - await PageObjects.discover.selectIndexPattern(SOURCE_DATA_INDEX); + await PageObjects.discover.selectIndexPattern(SOURCE_DATA_VIEW); await PageObjects.timePicker.setCommonlyUsedTime('Last_15 minutes'); await openDiscoverAlertFlyout(); @@ -307,7 +375,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('indexPattern-switcher--input'); const dataViewsElem = await testSubjects.find('euiSelectableList'); const sourceDataViewOption = await dataViewsElem.findByCssSelector( - `[title="${SOURCE_DATA_INDEX}"]` + `[title="${SOURCE_DATA_VIEW}"]` ); await sourceDataViewOption.click(); @@ -319,30 +387,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('ruleDetails-viewInApp'); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('navigate to discover', async () => { - const currentDataViewId = await PageObjects.discover.getCurrentDataViewId(); - return sourceDataViewId ? currentDataViewId === sourceDataViewId : true; - }); - - expect(await dataGrid.getDocCount()).to.be(5); + await checkInitialRuleParamsState(SOURCE_DATA_VIEW, true); }); it('should navigate to alert results via link provided in notification', async () => { - await openAlertResults(RULE_NAME, sourceDataViewId); - - expect(await toasts.getToastCount()).to.be.equal(1); - const content = await toasts.getToastContent(1); - expect(content).to.equal( - `Displayed documents may vary\nThe displayed documents might differ from the documents that triggered the alert. Some documents might have been added or deleted.` - ); - - const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView(); - expect(selectedDataView).to.be.equal('search-source-alert'); - - expect(await dataGrid.getDocCount()).to.be(5); + await openAlertResults(RULE_NAME); + await checkInitialRuleParamsState(SOURCE_DATA_VIEW); }); - it('should display warning about updated alert rule', async () => { + it('should display prev rule state after params update on clicking prev generated link', async () => { await openAlertRuleInManagement(RULE_NAME); // change rule configuration @@ -355,23 +408,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('saveEditedRuleButton'); await PageObjects.header.waitUntilLoadingHasFinished(); - await openAlertResults(RULE_NAME, sourceDataViewId); + await openAlertResults(RULE_NAME); + await checkInitialRuleParamsState(SOURCE_DATA_VIEW); + }); - const queryString = await queryBar.getQueryString(); - const hasFilter = await filterBar.hasFilter('message.keyword', 'msg-1'); - expect(queryString).to.be.equal('message:msg-1'); - expect(hasFilter).to.be.equal(true); + it('should display actual state after rule params update on clicking viewInApp link', async () => { + await clickViewInApp(RULE_NAME); - expect(await toasts.getToastCount()).to.be.equal(1); - const content = await toasts.getToastContent(1); - expect(content).to.equal( - `Alert rule has changed\nThe displayed documents might not match the documents that triggered the alert because the rule configuration changed.` - ); + const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView(); + expect(selectedDataView).to.be.equal(SOURCE_DATA_VIEW); - expect(await dataGrid.getDocCount()).to.be(1); + await checkUpdatedRuleParamsState(); }); - it('should display warning about recently updated data view', async () => { + it('should display prev data view state after update on clicking prev generated link', async () => { await PageObjects.common.navigateToUrlWithBrowserHistory( 'management', `/kibana/dataViews/dataView/${sourceDataViewId}`, @@ -379,49 +429,42 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await PageObjects.header.waitUntilLoadingHasFinished(); + // add source filter await testSubjects.click('tab-sourceFilters'); await testSubjects.click('fieldFilterInput'); - - const input = await find.activeElement(); - await input.type('message'); + const filtersInput = await find.activeElement(); + await filtersInput.type('message'); await testSubjects.click('addFieldFilterButton'); - await openAlertResults(RULE_NAME, sourceDataViewId); + // change title + await testSubjects.click('editIndexPatternButton'); + await testSubjects.setValue('createIndexPatternTitleInput', 'search-s', { + clearWithKeyboard: true, + typeCharByChar: true, + }); + await testSubjects.click('saveIndexPatternButton'); + await testSubjects.click('confirmModalConfirmButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); - expect(await toasts.getToastCount()).to.be(2); - const firstContent = await toasts.getToastContent(1); - expect(firstContent).to.equal( - `Data View has changed\nData view has been updated after the last update of the alert rule.` - ); - const secondContent = await toasts.getToastContent(2); - expect(secondContent).to.equal( - `Alert rule has changed\nThe displayed documents might not match the documents that triggered the alert because the rule configuration changed.` - ); + await openAlertResults(RULE_NAME); - expect(await dataGrid.getDocCount()).to.be(1); + await checkInitialRuleParamsState(SOURCE_DATA_VIEW); + await checkInitialDataViewState(SOURCE_DATA_VIEW); }); - it('should display not found index error', async () => { - await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_INDEX); - - await deleteDataView(sourceDataViewId); - - // rty to open alert results after index deletion - await openAlertResults(RULE_NAME); - - expect(await toasts.getToastCount()).to.be(1); - const firstContent = await toasts.getToastContent(1); - expect(firstContent).to.equal( - `Error fetching search source\nCould not locate that data view (id: ${sourceDataViewId}), click here to re-create it` - ); + it('should display actual data view state after update on clicking viewInApp link', async () => { + await clickViewInApp(RULE_NAME); + await checkUpdatedRuleParamsState(); + await checkUpdatedDataViewState('search-s*'); }); - it('should navigate to alert results via view in app link using adhoc data view', async () => { + it('should navigate to alert results via link provided in notification using adhoc data view', async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.createAdHocDataView('search-source-', true); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.timePicker.setCommonlyUsedTime('Last_15 minutes'); - await PageObjects.discover.addRuntimeField('runtime-message-field', `emit('mock-message')`); // create an alert @@ -429,16 +472,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await defineSearchSourceAlert('test-adhoc-alert'); await testSubjects.click('saveRuleButton'); await PageObjects.header.waitUntilLoadingHasFinished(); - sourceAdHocDataViewId = await PageObjects.discover.getCurrentDataViewId(); + await openAlertResults(ADHOC_RULE_NAME); + + const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView(); + expect(selectedDataView).to.be.equal('search-source-*'); + + const documentCell = await dataGrid.getCellElement(0, 3); + const firstRowContent = await documentCell.getVisibleText(); + expect(firstRowContent.includes('runtime-message-fieldmock-message_id')).to.be.equal(true); + + expect(await dataGrid.getDocCount()).to.be(5); + }); + + it('should navigate to alert results via view in app link using adhoc data view', async () => { // navigate to discover using view in app link - await openAlertRuleInManagement('test-adhoc-alert'); - await testSubjects.click('ruleDetails-viewInApp'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('navigate to discover', async () => { - const currentDataViewId = await PageObjects.discover.getCurrentDataViewId(); - return currentDataViewId === sourceAdHocDataViewId; - }); + await clickViewInApp(ADHOC_RULE_NAME); const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView(); expect(selectedDataView).to.be.equal('search-source-*'); @@ -448,18 +497,35 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(firstRowContent.includes('runtime-message-fieldmock-message_id')).to.be.equal(true); }); - it('should navigate to alert results via link provided in notification using adhoc data view', async () => { - await openAlertResults('test-adhoc-alert', sourceAdHocDataViewId); + it('should display results after data view removal on clicking prev generated link', async () => { + await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_VIEW); + await deleteDataView(sourceDataViewId); + + await openAlertResults(RULE_NAME); + + await checkInitialRuleParamsState(SOURCE_DATA_VIEW); + await checkInitialDataViewState(SOURCE_DATA_VIEW); + }); + + it('should not display results after data view removal on clicking viewInApp link', async () => { + await clickViewInApp(RULE_NAME); expect(await toasts.getToastCount()).to.be.equal(1); const content = await toasts.getToastContent(1); expect(content).to.equal( - `Displayed documents may vary\nThe displayed documents might differ from the documents that triggered the alert. Some documents might have been added or deleted.` + `Error fetching search source\nCould not locate that data view (id: ${sourceDataViewId}), click here to re-create it` ); - expect(await dataGrid.getDocCount()).to.be(5); + }); - const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView(); - expect(selectedDataView).to.be.equal('search-source-*'); + it('should display results after rule removal on following generated link', async () => { + await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_VIEW); + const [{ id: firstAlertId }] = await getAlertsByName(RULE_NAME); + await deleteAlerts([firstAlertId]); + + await openAlertResults(firstAlertId, 'id'); + + await checkInitialRuleParamsState(SOURCE_DATA_VIEW); + await checkInitialDataViewState(SOURCE_DATA_VIEW); }); }); }