[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:
Dmitry Tomashevich 2023-01-04 15:46:14 +03:00 committed by GitHub
parent 4e11ef1b6b
commit 503b466b72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 927 additions and 454 deletions

View file

@ -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',

View file

@ -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';

View file

@ -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,

View file

@ -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';

View file

@ -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>
)}

View file

@ -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>;
};

View file

@ -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',
}

View file

@ -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';

View file

@ -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,

View file

@ -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}`;

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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 {
/**

View file

@ -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',

View file

@ -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]);
};

View file

@ -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 {
/**

View file

@ -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';

View file

@ -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;
}

View file

@ -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,
};
};

View file

@ -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

View file

@ -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',
}

View file

@ -7,4 +7,3 @@
*/
export { DocumentViewModeToggle } from './view_mode_toggle';
export { VIEW_MODE } from './constants';

View file

@ -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', () => {

View file

@ -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';

View file

@ -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;

View file

@ -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';

View file

@ -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,

View file

@ -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(

View file

@ -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';

View file

@ -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 {};
}

View file

@ -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,

View file

@ -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) ?? '';
}
}

View file

@ -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,

View file

@ -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);

View file

@ -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:'')"`
);
});
});
});

View file

@ -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);
}

View file

@ -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', () => {

View file

@ -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}`;
}

View file

@ -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,

View file

@ -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;

View file

@ -7,7 +7,6 @@
*/
export {
encodeState,
decodeState,
expandedStateToHashedState,
hashedStateToExpandedState,

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { isStateHash, createStateHash, persistState, retrieveState } from './state_hash';
export { persistState, retrieveState } from './state_hash';

View file

@ -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;

View file

@ -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>;

View file

@ -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';

View file

@ -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

View file

@ -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';

View file

@ -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) {

View file

@ -12,6 +12,8 @@
"requiredPlugins": [
"actions",
"data",
"dataViews",
"share",
"encryptedSavedObjects",
"eventLog",
"features",

View file

@ -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>;

View file

@ -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 = {

View file

@ -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,

View file

@ -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(),

View file

@ -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,

View file

@ -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(),

View file

@ -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),

View file

@ -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;

View file

@ -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;
}

View file

@ -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/**/*",

View file

@ -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;

View file

@ -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';

View file

@ -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;

View file

@ -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';

View file

@ -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(),
};
});

View file

@ -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,

View file

@ -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,

View file

@ -31,6 +31,7 @@
"@kbn/logging-mocks",
"@kbn/logging",
"@kbn/securitysolution-io-ts-utils",
"@kbn/share-plugin",
],
"exclude": [
"target/**/*",

View file

@ -16,6 +16,7 @@
"cloudSecurityPosture",
"dashboard",
"data",
"dataViews",
"embeddable",
"eventLog",
"features",

View file

@ -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(),

View file

@ -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

View file

@ -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],

View file

@ -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,

View file

@ -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;

View file

@ -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(
{

View file

@ -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,
};
}

View file

@ -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'
);

View file

@ -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;
}
});
}

View file

@ -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({

View file

@ -40,6 +40,8 @@
"@kbn/react-field",
"@kbn/core-elasticsearch-server-mocks",
"@kbn/logging-mocks",
"@kbn/share-plugin",
"@kbn/discover-plugin",
],
"exclude": [
"target/**/*",

View file

@ -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';

View file

@ -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';

View file

@ -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 dextraire 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",

View file

@ -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": "フィールド統計情報",

View file

@ -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": "字段统计信息",

View file

@ -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);
});
});
}