mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Discover][Alerting] Use Discover locator for alert results link (#146403)
## Summary Closes #145815, #134232 - Moves Discover locator to common area - Builds alerts results link from the server - Now there are two implementations of `setStateToKbnUrl` which is used in locator. New one in common are lost `HashedItemStore` support, since sessions storage are actual only for browser - Toasts `Alert rule has changed`, `Data View has changed` removed - link generated per each alert will be unique representation of those `rule params` and `data view state` which were at the time of invocation - Restuls link will live even after data view and rule removal ### How to create rule - Create an output index and data view `test` <details> <summary>Query to use</summary> ``` PUT test { "settings" : { "number_of_shards" : 1 }, "mappings" : { "properties" : { "rule_id" : { "type" : "text" }, "rule_name" : { "type" : "text" }, "alert_id" : { "type" : "text" }, "context_message": { "type" : "text" } } } } ``` </details> - Create alerts connector using `test` index - Open `Elasticsearch query` alert in `KQL or Lucene` mode or just using Discover `Alerts` button - Specify the following params: `IS ABOVE: 1`, `FOR THE LAST: 30 min` - Try execute it by clicking `Test query`. It should match some results - When choosing connector, use the following config ``` { "rule_id": "{{rule.id}}", "rule_name": "{{rule.name}}", "alert_id": "{{alert.id}}", "context_message": "{{context.message}}" } ``` - Create the alert ### How to test - Create `Elasticsearch query` rule in `KQL or Lucene` mode like described above - Wait for some seconds and find the triggered alert document by browsing `test` data view in Discover. There should be a link to results in `context_message` field. Save the link somewhere - Change rule params by adding/removing filters / changing query / changing data view - Follow saved link, you should see previous filters, query and data view state - Open rule in management and click `View in app`, you should see actual state of rule - Try to remove used data view and then follow saved link, you should still see the results - Try to remove rule and then follow saved link, you should still see the results. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
This commit is contained in:
parent
4e11ef1b6b
commit
503b466b72
90 changed files with 927 additions and 454 deletions
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -210,7 +210,9 @@ export const EditIndexPattern = withRouter(
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiText size="s">{indexPatternHeading}</EuiText>
|
||||
<EuiCode style={codeStyle}>{indexPattern.title}</EuiCode>
|
||||
<EuiCode data-test-subj="currentIndexPatternTitle" style={codeStyle}>
|
||||
{indexPattern.title}
|
||||
</EuiCode>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
@ -218,7 +220,9 @@ export const EditIndexPattern = withRouter(
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiText size="s">{timeFilterHeading}</EuiText>
|
||||
<EuiCode style={codeStyle}>{indexPattern.timeFieldName}</EuiCode>
|
||||
<EuiCode data-test-subj="currentIndexPatternTimeField" style={codeStyle}>
|
||||
{indexPattern.timeFieldName}
|
||||
</EuiCode>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
|
|
@ -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<DataViewsContract>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
|
@ -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<DiscoverAppLocatorParams>;
|
||||
|
||||
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<DiscoverAppLocatorParams> {
|
||||
|
@ -134,6 +140,7 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition<DiscoverA
|
|||
viewMode,
|
||||
hideAggregatedPreview,
|
||||
breakdownField,
|
||||
isAlertResults,
|
||||
} = params;
|
||||
const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : '';
|
||||
const appState: {
|
||||
|
@ -168,13 +175,12 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition<DiscoverA
|
|||
if (breakdownField) appState.breakdownField = breakdownField;
|
||||
|
||||
const state: MainHistoryLocationState = {};
|
||||
if (dataViewSpec) {
|
||||
state.dataViewSpec = dataViewSpec;
|
||||
}
|
||||
if (dataViewSpec) state.dataViewSpec = dataViewSpec;
|
||||
if (isAlertResults) state.isAlertResults = isAlertResults;
|
||||
|
||||
let path = `#/${savedSearchPath}`;
|
||||
path = setStateToKbnUrl<GlobalQueryStateFromUrl>('_g', queryState, { useHash }, path);
|
||||
path = setStateToKbnUrl('_a', appState, { useHash }, path);
|
||||
path = this.deps.setStateToKbnUrl<GlobalQueryStateFromUrl>('_g', queryState, { useHash }, path);
|
||||
path = this.deps.setStateToKbnUrl('_a', appState, { useHash }, path);
|
||||
|
||||
if (searchSessionId) {
|
||||
path = `${path}&searchSessionId=${searchSessionId}`;
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(<MarkdownSimple>{infoDescription}</MarkdownSimple>),
|
||||
});
|
||||
};
|
||||
|
||||
export const useAlertResultsToast = ({
|
||||
isAlertResults,
|
||||
toastNotifications,
|
||||
}: {
|
||||
isAlertResults?: boolean;
|
||||
toastNotifications: ToastsStart;
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (isAlertResults) {
|
||||
displayPossibleDocsDiffInfoAlert(toastNotifications);
|
||||
}
|
||||
}, [isAlertResults, toastNotifications]);
|
||||
};
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
const DISCOVER_MAIN_ROUTE = '/';
|
||||
|
||||
type NonNullableEntry<T> = { [K in keyof T]: NonNullable<T[keyof T]> };
|
||||
|
||||
const getCurrentChecksum = (params: SearchThresholdAlertParams) =>
|
||||
sha256.create().update(JSON.stringify(params)).hex();
|
||||
|
||||
const isActualAlert = (queryParams: QueryParams): queryParams is NonNullableEntry<QueryParams> => {
|
||||
return Boolean(queryParams.from && queryParams.to && queryParams.checksum);
|
||||
return Boolean(queryParams.from && queryParams.to);
|
||||
};
|
||||
|
||||
const buildTimeRangeFilter = (
|
||||
dataView: DataView,
|
||||
fetchedAlert: Rule<SearchThresholdAlertParams>,
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
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 { fetchAlert, fetchSearchSource, buildLocatorParams } = getAlertUtils(
|
||||
openActualAlert,
|
||||
queryParams,
|
||||
toastNotifications,
|
||||
core,
|
||||
data
|
||||
);
|
||||
|
||||
const navigateToResults = async () => {
|
||||
const fetchedAlert = await fetchAlert(id);
|
||||
if (!fetchedAlert) {
|
||||
history.push(DISCOVER_MAIN_ROUTE);
|
||||
return;
|
||||
const navigateWithDiscoverState = (state: DiscoverAppLocatorParams) => {
|
||||
if (openActualAlert) {
|
||||
displayPossibleDocsDiffInfoAlert(toastNotifications);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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<SearchThresholdAlertParams>,
|
||||
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(<MarkdownSimple>{warnDescription}</MarkdownSimple>),
|
||||
});
|
||||
};
|
||||
|
||||
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(<MarkdownSimple>{infoDescription}</MarkdownSimple>),
|
||||
});
|
||||
};
|
||||
|
||||
const fetchAlert = async (id: string) => {
|
||||
try {
|
||||
return await core.http.get<Rule<SearchThresholdAlertParams>>(
|
||||
|
@ -89,12 +78,18 @@ export const getAlertUtils = (
|
|||
title: errorTitle,
|
||||
text: toMountPoint(<MarkdownSimple>{error.message}</MarkdownSimple>),
|
||||
});
|
||||
throw new Error(errorTitle);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSearchSource = async (fetchedAlert: Rule<SearchThresholdAlertParams>) => {
|
||||
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(<MarkdownSimple>{error.message}</MarkdownSimple>),
|
||||
});
|
||||
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<SearchThresholdAlertParams>;
|
||||
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(<MarkdownSimple>{warnDescription}</MarkdownSimple>),
|
||||
});
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
}
|
|
@ -7,4 +7,3 @@
|
|||
*/
|
||||
|
||||
export { DocumentViewModeToggle } from './view_mode_toggle';
|
||||
export { VIEW_MODE } from './constants';
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<object, object> {
|
||||
public setup(
|
||||
|
@ -19,6 +22,7 @@ export class DiscoverServerPlugin implements Plugin<object, object> {
|
|||
plugins: {
|
||||
data: DataPluginSetup;
|
||||
home?: HomeServerPluginSetup;
|
||||
share?: SharePluginSetup;
|
||||
}
|
||||
) {
|
||||
core.capabilities.registerProvider(capabilitiesProvider);
|
||||
|
@ -28,6 +32,12 @@ export class DiscoverServerPlugin implements Plugin<object, object> {
|
|||
registerSampleData(plugins.home.sampleData);
|
||||
}
|
||||
|
||||
if (plugins.share) {
|
||||
plugins.share.url.locators.create(
|
||||
new DiscoverAppLocatorDefinition({ useHash: false, setStateToKbnUrl })
|
||||
);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<State extends RisonValue> 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: State,
|
||||
useHash: boolean,
|
||||
createHash: (rawState: State) => string
|
||||
): string {
|
||||
if (useHash) {
|
||||
return createHash(state);
|
||||
} else {
|
||||
return rison.encodeUnknown(state) ?? '';
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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);
|
|
@ -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:'')"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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: <State>(rawState: State) => string) {
|
||||
return <State>(
|
||||
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(<State>(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<State>(
|
||||
key: string,
|
||||
state: State,
|
||||
hashOptions: SetStateToKbnUrlHashOptions,
|
||||
rawUrl: string
|
||||
): string {
|
||||
return internalSetStateToKbnUrl(key, state, hashOptions, rawUrl);
|
||||
}
|
|
@ -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', () => {
|
|
@ -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}`;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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<State extends RisonValue>(expandedOrHashedState: string)
|
||||
|
@ -21,21 +23,9 @@ export function decodeState<State>(expandedOrHashedState: string): State {
|
|||
}
|
||||
}
|
||||
|
||||
// should be:
|
||||
// export function encodeState<State extends RisonValue> 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: 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;
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
export {
|
||||
encodeState,
|
||||
decodeState,
|
||||
expandedStateToHashedState,
|
||||
hashedStateToExpandedState,
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { isStateHash, createStateHash, persistState, retrieveState } from './state_hash';
|
||||
export { persistState, retrieveState } from './state_hash';
|
||||
|
|
|
@ -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<State>(stateHash: string): State {
|
||||
const json = hashedItemStore.getItem(stateHash);
|
||||
const throwUnableToRestoreUrlError = () => {
|
||||
|
@ -65,7 +32,7 @@ export function retrieveState<State>(stateHash: string): State {
|
|||
|
||||
export function persistState<State>(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;
|
||||
|
|
|
@ -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<string, any>;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<State>(
|
|||
* 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<State>(
|
||||
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
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
"requiredPlugins": [
|
||||
"actions",
|
||||
"data",
|
||||
"dataViews",
|
||||
"share",
|
||||
"encryptedSavedObjects",
|
||||
"eventLog",
|
||||
"features",
|
||||
|
|
|
@ -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<PluginStartContract> = {
|
||||
listTypes: jest.fn(),
|
||||
|
@ -148,6 +165,8 @@ const createRuleExecutorServicesMock = <
|
|||
search: createAbortableSearchServiceMock(),
|
||||
searchSourceClient: searchSourceCommonMock,
|
||||
ruleMonitoringService: createRuleMonitoringServiceMock(),
|
||||
share: createShareStartMock(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
};
|
||||
};
|
||||
export type RuleExecutorServicesMock = ReturnType<typeof createRuleExecutorServicesMock>;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<TaskRunnerContext> & {
|
||||
actionsPlugin: jest.Mocked<ActionsPluginStart>;
|
||||
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<typeof loggingSystemMock.createLogger> = 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(),
|
||||
|
|
|
@ -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<TaskRunnerContext> = {
|
||||
data: dataPlugin,
|
||||
dataViews: dataViewsMock,
|
||||
savedObjects: savedObjectsService,
|
||||
share: {} as SharePluginStart,
|
||||
uiSettings: uiSettingsService,
|
||||
elasticsearch: elasticsearchService,
|
||||
getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -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<typeof createRule>;
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"@kbn/logging-mocks",
|
||||
"@kbn/logging",
|
||||
"@kbn/securitysolution-io-ts-utils",
|
||||
"@kbn/share-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"cloudSecurityPosture",
|
||||
"dashboard",
|
||||
"data",
|
||||
"dataViews",
|
||||
"embeddable",
|
||||
"eventLog",
|
||||
"features",
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -32,9 +32,10 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
spaceId,
|
||||
logger,
|
||||
} = options;
|
||||
const { alertFactory, scopedClusterClient, searchSourceClient } = services;
|
||||
const { alertFactory, scopedClusterClient, searchSourceClient, share, dataViews } = services;
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
const publicBaseUrl = core.http.basePath.publicBaseUrl ?? '';
|
||||
const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : '';
|
||||
const alertLimit = alertFactory.alertLimit.getValue();
|
||||
const compareFn = ComparatorFns.get(params.thresholdComparator);
|
||||
if (compareFn == null) {
|
||||
|
@ -49,13 +50,15 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
// avoid counting a document multiple times.
|
||||
// latestTimestamp will be ignored if set for grouped queries
|
||||
let latestTimestamp: string | undefined = tryToParseAsDate(state.latestTimestamp);
|
||||
const { parsedResults, dateStart, dateEnd } = esQueryRule
|
||||
const { parsedResults, dateStart, dateEnd, link } = esQueryRule
|
||||
? await fetchEsQuery({
|
||||
ruleId,
|
||||
name,
|
||||
alertLimit,
|
||||
params: params as OnlyEsQueryRuleParams,
|
||||
timestamp: latestTimestamp,
|
||||
publicBaseUrl,
|
||||
spacePrefix,
|
||||
services: {
|
||||
scopedClusterClient,
|
||||
logger,
|
||||
|
@ -66,19 +69,15 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
|
|||
alertLimit,
|
||||
params: params as OnlySearchSourceRuleParams,
|
||||
latestTimestamp,
|
||||
spacePrefix,
|
||||
services: {
|
||||
share,
|
||||
searchSourceClient,
|
||||
logger,
|
||||
dataViews,
|
||||
},
|
||||
});
|
||||
|
||||
const base = publicBaseUrl;
|
||||
const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : '';
|
||||
const link = esQueryRule
|
||||
? `${base}${spacePrefix}/app/management/insightsAndAlerting/triggersActions/rule/${ruleId}`
|
||||
: `${base}${spacePrefix}/app/discover#/viewAlert/${ruleId}?from=${dateStart}&to=${dateEnd}&checksum=${getChecksum(
|
||||
params as OnlyEsQueryRuleParams
|
||||
)}`;
|
||||
const unmetGroupValues: Record<string, number> = {};
|
||||
for (const result of parsedResults.results) {
|
||||
const alertId = result.group;
|
||||
|
|
|
@ -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(
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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<DiscoverAppLocatorParams>('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<DiscoverAppLocatorParams>,
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<unknown, {}> = 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({
|
||||
|
|
|
@ -40,6 +40,8 @@
|
|||
"@kbn/react-field",
|
||||
"@kbn/core-elasticsearch-server-mocks",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/share-plugin",
|
||||
"@kbn/discover-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "フィールド統計情報",
|
||||
|
|
|
@ -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": "字段统计信息",
|
||||
|
|
|
@ -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);
|
||||
|
||||
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 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.`
|
||||
);
|
||||
|
||||
expect(await dataGrid.getDocCount()).to.be(1);
|
||||
await openAlertResults(RULE_NAME);
|
||||
await checkInitialRuleParamsState(SOURCE_DATA_VIEW);
|
||||
});
|
||||
|
||||
it('should display warning about recently updated data view', async () => {
|
||||
it('should display actual state after rule params update on clicking viewInApp link', async () => {
|
||||
await clickViewInApp(RULE_NAME);
|
||||
|
||||
const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
|
||||
expect(selectedDataView).to.be.equal(SOURCE_DATA_VIEW);
|
||||
|
||||
await checkUpdatedRuleParamsState();
|
||||
});
|
||||
|
||||
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.`
|
||||
);
|
||||
|
||||
expect(await dataGrid.getDocCount()).to.be(1);
|
||||
});
|
||||
|
||||
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`
|
||||
);
|
||||
await checkInitialRuleParamsState(SOURCE_DATA_VIEW);
|
||||
await checkInitialDataViewState(SOURCE_DATA_VIEW);
|
||||
});
|
||||
|
||||
it('should navigate to alert results via view in app link using adhoc data view', async () => {
|
||||
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 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue