{tabs.map((tab) => (
setSelectedTabId(tab.id)}
key={tab.id}
@@ -146,6 +147,6 @@ export const DecisionPathPopover: FC = ({
{selectedTabId === DECISION_PATH_TABS.JSON && (
)}
- >
+
);
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx
index 64835e7ca4c6d..0fab1cf75259e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx
@@ -210,6 +210,7 @@ export const FeatureImportanceSummaryPanel: FC
-
+
)
}
/>
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
index 17ef84179ce63..63b7074ec3aaa 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
@@ -7,10 +7,10 @@
import React, { FC, useCallback, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import {
+ EuiInMemoryTable,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
- EuiInMemoryTable,
EuiSearchBar,
EuiSearchBarProps,
EuiSpacer,
@@ -30,13 +30,12 @@ import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns';
import { ExpandedRow } from './expanded_row';
import { AnalyticStatsBarStats, StatsBar } from '../../../../../components/stats_bar';
import { CreateAnalyticsButton } from '../create_analytics_button';
-import { getSelectedIdFromUrl } from '../../../../../jobs/jobs_list/components/utils';
import { SourceSelection } from '../source_selection';
import { filterAnalytics } from '../../../../common/search_bar_filters';
import { AnalyticsEmptyPrompt } from './empty_prompt';
import { useTableSettings } from './use_table_settings';
import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button';
-import { getGroupQueryText } from '../../../../../../../common/util/string_utils';
+import { ListingPageUrlState } from '../../../../../../../common/types/common';
const filters: EuiSearchBarProps['filters'] = [
{
@@ -84,17 +83,28 @@ interface Props {
isManagementTable?: boolean;
isMlEnabledInSpace?: boolean;
blockRefresh?: boolean;
+ pageState: ListingPageUrlState;
+ updatePageState: (update: Partial) => void;
}
export const DataFrameAnalyticsList: FC = ({
isManagementTable = false,
isMlEnabledInSpace = true,
blockRefresh = false,
+ pageState,
+ updatePageState,
}) => {
+ const searchQueryText = pageState.queryText ?? '';
+ const setSearchQueryText = useCallback(
+ (value) => {
+ updatePageState({ queryText: value });
+ },
+ [updatePageState]
+ );
+
const [isInitialized, setIsInitialized] = useState(false);
const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [filteredAnalytics, setFilteredAnalytics] = useState([]);
- const [searchQueryText, setSearchQueryText] = useState('');
const [searchError, setSearchError] = useState();
const [analytics, setAnalytics] = useState([]);
const [analyticsStats, setAnalyticsStats] = useState(
@@ -102,9 +112,6 @@ export const DataFrameAnalyticsList: FC = ({
);
const [expandedRowItemIds, setExpandedRowItemIds] = useState([]);
const [errorMessage, setErrorMessage] = useState(undefined);
- // Query text/job_id based on url but only after getAnalytics is done first
- // selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly
- const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false);
const disabled =
!checkPermission('canCreateDataFrameAnalytics') ||
@@ -119,17 +126,20 @@ export const DataFrameAnalyticsList: FC = ({
isManagementTable
);
- const updateFilteredItems = (queryClauses: any) => {
- if (queryClauses.length) {
- const filtered = filterAnalytics(analytics, queryClauses);
- setFilteredAnalytics(filtered);
- } else {
- setFilteredAnalytics(analytics);
- }
- };
+ const updateFilteredItems = useCallback(
+ (queryClauses: any[]) => {
+ if (queryClauses.length) {
+ const filtered = filterAnalytics(analytics, queryClauses);
+ setFilteredAnalytics(filtered);
+ } else {
+ setFilteredAnalytics(analytics);
+ }
+ },
+ [analytics]
+ );
const filterList = () => {
- if (searchQueryText !== '' && selectedIdFromUrlInitialized === true) {
+ if (searchQueryText !== '') {
// trigger table filtering with query for job id to trigger table filter
const query = EuiSearchBar.Query.parse(searchQueryText);
let clauses: any = [];
@@ -142,27 +152,9 @@ export const DataFrameAnalyticsList: FC = ({
}
};
- useEffect(() => {
- if (selectedIdFromUrlInitialized === false && analytics.length > 0) {
- const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href);
- let queryText = '';
-
- if (groupIds !== undefined) {
- queryText = getGroupQueryText(groupIds);
- } else if (jobId !== undefined) {
- queryText = jobId;
- }
-
- setSelectedIdFromUrlInitialized(true);
- setSearchQueryText(queryText);
- } else {
- filterList();
- }
- }, [selectedIdFromUrlInitialized, analytics]);
-
useEffect(() => {
filterList();
- }, [selectedIdFromUrlInitialized, searchQueryText]);
+ }, [searchQueryText]);
const getAnalyticsCallback = useCallback(() => getAnalytics(true), []);
@@ -183,19 +175,19 @@ export const DataFrameAnalyticsList: FC = ({
);
const { onTableChange, pagination, sorting } = useTableSettings(
- DataFrameAnalyticsListColumn.id,
- filteredAnalytics
+ filteredAnalytics,
+ pageState,
+ updatePageState
);
const handleSearchOnChange: EuiSearchBarProps['onChange'] = (search) => {
if (search.error !== null) {
setSearchError(search.error.message);
- return false;
+ return;
}
setSearchError(undefined);
setSearchQueryText(search.queryText);
- return true;
};
// Before the analytics have been loaded for the first time, display the loading indicator only.
@@ -251,6 +243,7 @@ export const DataFrameAnalyticsList: FC = ({
);
+
const search: EuiSearchBarProps = {
query: searchQueryText,
onChange: handleSearchOnChange,
@@ -284,15 +277,13 @@ export const DataFrameAnalyticsList: FC = ({
allowNeutralSort={false}
- className="mlAnalyticsInMemoryTable"
columns={columns}
- error={searchError}
hasActions={false}
isExpandable={true}
+ itemIdToExpandedRowMap={itemIdToExpandedRowMap}
isSelectable={false}
items={analytics}
itemId={DataFrameAnalyticsListColumn.id}
- itemIdToExpandedRowMap={itemIdToExpandedRowMap}
loading={isLoading}
onTableChange={onTableChange}
pagination={pagination}
@@ -302,6 +293,7 @@ export const DataFrameAnalyticsList: FC = ({
rowProps={(item) => ({
'data-test-subj': `mlAnalyticsTableRow row-${item.id}`,
})}
+ error={searchError}
/>
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts
index 8c7c8b9db8b64..84c37ac8b816b 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts
@@ -116,14 +116,14 @@ export interface DataFrameAnalyticsListRow {
}
// Used to pass on attribute names to table columns
-export enum DataFrameAnalyticsListColumn {
- configDestIndex = 'config.dest.index',
- configSourceIndex = 'config.source.index',
- configCreateTime = 'config.create_time',
- description = 'config.description',
- id = 'id',
- memoryStatus = 'stats.memory_usage.status',
-}
+export const DataFrameAnalyticsListColumn = {
+ configDestIndex: 'config.dest.index',
+ configSourceIndex: 'config.source.index',
+ configCreateTime: 'config.create_time',
+ description: 'config.description',
+ id: 'id',
+ memoryStatus: 'stats.memory_usage.status',
+} as const;
export type ItemIdToExpandedRowMap = Record;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx
index 2b63b9e780819..93868ce0c17e6 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx
@@ -135,13 +135,13 @@ export const progressColumn = {
'data-test-subj': 'mlAnalyticsTableColumnProgress',
};
-export const DFAnalyticsJobIdLink = ({ item }: { item: DataFrameAnalyticsListRow }) => {
+export const DFAnalyticsJobIdLink = ({ jobId }: { jobId: string }) => {
const href = useMlLink({
page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE,
- pageState: { jobId: item.id },
+ pageState: { jobId },
});
- return {item.id};
+ return {jobId};
};
export const useColumns = (
@@ -199,13 +199,17 @@ export const useColumns = (
'data-test-subj': 'mlAnalyticsTableRowDetailsToggle',
},
{
- name: 'ID',
+ field: DataFrameAnalyticsListColumn.id,
+ name: i18n.translate('xpack.ml.dataframe.analyticsList.id', {
+ defaultMessage: 'ID',
+ }),
sortable: (item: DataFrameAnalyticsListRow) => item.id,
truncateText: true,
'data-test-subj': 'mlAnalyticsTableColumnId',
scope: 'row',
- render: (item: DataFrameAnalyticsListRow) =>
- isManagementTable ? : item.id,
+ render: (jobId: string) => {
+ return isManagementTable ? : jobId;
+ },
},
{
field: DataFrameAnalyticsListColumn.description,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts
index 5b7d71dacccf8..68774fb86fe96 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts
@@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useState } from 'react';
import { Direction, EuiBasicTableProps, Pagination, PropertySort } from '@elastic/eui';
+import { useCallback, useMemo } from 'react';
+import { ListingPageUrlState } from '../../../../../../../common/types/common';
-const PAGE_SIZE = 10;
const PAGE_SIZE_OPTIONS = [10, 25, 50];
// Copying from EUI EuiBasicTable types as type is not correctly picked up for table's onChange
@@ -29,15 +29,6 @@ export interface CriteriaWithPagination extends Criteria {
};
}
-interface AnalyticsBasicTableSettings {
- pageIndex: number;
- pageSize: number;
- totalItemCount: number;
- hidePerPageOptions: boolean;
- sortField: keyof T;
- sortDirection: Direction;
-}
-
interface UseTableSettingsReturnValue {
onTableChange: EuiBasicTableProps['onChange'];
pagination: Pagination;
@@ -45,49 +36,44 @@ interface UseTableSettingsReturnValue {
}
export function useTableSettings(
- sortByField: keyof TypeOfItem,
- items: TypeOfItem[]
+ items: TypeOfItem[],
+ pageState: ListingPageUrlState,
+ updatePageState: (update: Partial) => void
): UseTableSettingsReturnValue {
- const [tableSettings, setTableSettings] = useState>({
- pageIndex: 0,
- pageSize: PAGE_SIZE,
- totalItemCount: 0,
- hidePerPageOptions: false,
- sortField: sortByField,
- sortDirection: 'asc',
- });
-
- const onTableChange: EuiBasicTableProps['onChange'] = ({
- page = { index: 0, size: PAGE_SIZE },
- sort = { field: sortByField, direction: 'asc' },
- }: CriteriaWithPagination) => {
- const { index, size } = page;
- const { field, direction } = sort;
-
- setTableSettings({
- ...tableSettings,
- pageIndex: index,
- pageSize: size,
- sortField: field,
- sortDirection: direction,
- });
- };
+ const { pageIndex, pageSize, sortField, sortDirection } = pageState;
- const { pageIndex, pageSize, sortField, sortDirection } = tableSettings;
+ const onTableChange: EuiBasicTableProps['onChange'] = useCallback(
+ ({ page, sort }: CriteriaWithPagination) => {
+ const result = {
+ pageIndex: page?.index ?? pageState.pageIndex,
+ pageSize: page?.size ?? pageState.pageSize,
+ sortField: (sort?.field as string) ?? pageState.sortField,
+ sortDirection: sort?.direction ?? pageState.sortDirection,
+ };
+ updatePageState(result);
+ },
+ [pageState, updatePageState]
+ );
- const pagination = {
- pageIndex,
- pageSize,
- totalItemCount: items.length,
- pageSizeOptions: PAGE_SIZE_OPTIONS,
- };
+ const pagination = useMemo(
+ () => ({
+ pageIndex,
+ pageSize,
+ totalItemCount: items.length,
+ pageSizeOptions: PAGE_SIZE_OPTIONS,
+ }),
+ [items, pageIndex, pageSize]
+ );
- const sorting = {
- sort: {
- field: sortField as string,
- direction: sortDirection,
- },
- };
+ const sorting = useMemo(
+ () => ({
+ sort: {
+ field: sortField as string,
+ direction: sortDirection as Direction,
+ },
+ }),
+ [sortField, sortDirection]
+ );
return { onTableChange, pagination, sorting };
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx
index eaeae6cc64520..a5d3555fcc278 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx
@@ -50,9 +50,12 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string
return navTabs;
}, [jobId !== undefined]);
- const onTabClick = useCallback(async (tab: Tab) => {
- await navigateToPath(tab.path, true);
- }, []);
+ const onTabClick = useCallback(
+ async (tab: Tab) => {
+ await navigateToPath(tab.path, true);
+ },
+ [navigateToPath]
+ );
return (
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts
index 7c70a25071640..77c794dce10ce 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts
@@ -6,8 +6,8 @@
export * from './models_list';
-export enum ModelsTableToConfigMapping {
- id = 'model_id',
- createdAt = 'create_time',
- type = 'type',
-}
+export const ModelsTableToConfigMapping = {
+ id: 'model_id',
+ createdAt: 'create_time',
+ type: 'type',
+} as const;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
index a87f11df937d3..2d74d08c4550c 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
@@ -52,6 +52,8 @@ import { filterAnalyticsModels } from '../../../../common/search_bar_filters';
import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
import { timeFormatter } from '../../../../../../../common/util/date_utils';
+import { ListingPageUrlState } from '../../../../../../../common/types/common';
+import { usePageUrlState } from '../../../../../util/url_state';
type Stats = Omit;
@@ -63,6 +65,13 @@ export type ModelItem = TrainedModelConfigResponse & {
export type ModelItemFull = Required;
+export const getDefaultModelsListState = (): ListingPageUrlState => ({
+ pageIndex: 0,
+ pageSize: 10,
+ sortField: ModelsTableToConfigMapping.id,
+ sortDirection: 'asc',
+});
+
export const ModelsList: FC = () => {
const {
services: {
@@ -71,12 +80,24 @@ export const ModelsList: FC = () => {
} = useMlKibana();
const urlGenerator = useMlUrlGenerator();
+ const [pageState, updatePageState] = usePageUrlState(
+ ML_PAGES.DATA_FRAME_ANALYTICS_MODELS_MANAGE,
+ getDefaultModelsListState()
+ );
+
+ const searchQueryText = pageState.queryText ?? '';
+ const setSearchQueryText = useCallback(
+ (value) => {
+ updatePageState({ queryText: value });
+ },
+ [updatePageState]
+ );
+
const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean;
const trainedModelsApiService = useTrainedModelsApiService();
const { toasts } = useNotifications();
- const [searchQueryText, setSearchQueryText] = useState('');
const [filteredModels, setFilteredModels] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [items, setItems] = useState([]);
@@ -432,8 +453,9 @@ export const ModelsList: FC = () => {
: [];
const { onTableChange, pagination, sorting } = useTableSettings(
- ModelsTableToConfigMapping.id,
- filteredModels
+ filteredModels,
+ pageState,
+ updatePageState
);
const toolsLeft = (
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx
index 44085384f7536..5a17b91818a1c 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx
@@ -33,11 +33,27 @@ import { UpgradeWarning } from '../../../components/upgrade';
import { AnalyticsNavigationBar } from './components/analytics_navigation_bar';
import { ModelsList } from './components/models_management';
import { JobMap } from '../job_map';
+import { usePageUrlState } from '../../../util/url_state';
+import { ListingPageUrlState } from '../../../../../common/types/common';
+import { DataFrameAnalyticsListColumn } from './components/analytics_list/common';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
+
+export const getDefaultDFAListState = (): ListingPageUrlState => ({
+ pageIndex: 0,
+ pageSize: 10,
+ sortField: DataFrameAnalyticsListColumn.id,
+ sortDirection: 'asc',
+});
export const Page: FC = () => {
const [blockRefresh, setBlockRefresh] = useState(false);
const [globalState] = useUrlState('_g');
+ const [dfaPageState, setDfaPageState] = usePageUrlState(
+ ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE,
+ getDefaultDFAListState()
+ );
+
useRefreshInterval(setBlockRefresh);
const location = useLocation();
@@ -59,16 +75,13 @@ export const Page: FC = () => {
/>
@@ -96,7 +109,11 @@ export const Page: FC = () => {
{selectedTabId === 'map' && mapJobId && }
{selectedTabId === 'data_frame_analytics' && (
-
+
)}
{selectedTabId === 'models' && }
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
index 39166841a4e1b..95c721a7043dc 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
@@ -123,7 +123,8 @@ export const anomalyDataChange = function (
config.timeField,
range.min,
range.max,
- bucketSpanSeconds * 1000
+ bucketSpanSeconds * 1000,
+ config.datafeedConfig
)
.toPromise();
} else {
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx
index f0fa62b7a3d8a..1b1bea889925f 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx
@@ -22,8 +22,6 @@ import { JobGroup } from '../job_group';
import { useMlKibana } from '../../../../contexts/kibana';
interface JobFilterBarProps {
- jobId: string;
- groupIds: string[];
setFilters: (query: Query | null) => void;
queryText?: string;
}
@@ -75,7 +73,7 @@ export const JobFilterBar: FC = ({ queryText, setFilters }) =
useEffect(() => {
setFilters(queryInstance);
- }, []);
+ }, [queryText]);
const filters: SearchFilterConfig[] = useMemo(
() => [
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js
index 397062248689d..338222e3ac4a2 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js
@@ -6,7 +6,6 @@
import { each } from 'lodash';
import { i18n } from '@kbn/i18n';
-import rison from 'rison-node';
import { mlJobService } from '../../../services/job_service';
import {
@@ -367,31 +366,3 @@ function jobProperty(job, prop) {
};
return job[propMap[prop]];
}
-
-function getUrlVars(url) {
- const vars = {};
- url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (_, key, value) {
- vars[key] = value;
- });
- return vars;
-}
-
-export function getSelectedIdFromUrl(url) {
- const result = {};
- if (typeof url === 'string') {
- const isGroup = url.includes('groupIds');
- url = decodeURIComponent(url);
-
- if (url.includes('mlManagement')) {
- const urlParams = getUrlVars(url);
- const decodedJson = rison.decode(urlParams.mlManagement);
-
- if (isGroup) {
- result.groupIds = decodedJson.groupIds;
- } else {
- result.jobId = decodedJson.jobId;
- }
- }
- }
- return result;
-}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts
deleted file mode 100644
index 4414be0b4fdcb..0000000000000
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts
+++ /dev/null
@@ -1,35 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { getSelectedIdFromUrl } from './utils';
-
-describe('ML - Jobs List utils', () => {
- const jobId = 'test_job_id_1';
- const jobIdUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(jobId:${jobId})`;
- const groupIdOne = 'test_group_id_1';
- const groupIdTwo = 'test_group_id_2';
- const groupIdsUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(groupIds:!(${groupIdOne},${groupIdTwo}))`;
- const groupIdUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(groupIds:!(${groupIdOne}))`;
-
- describe('getSelectedIdFromUrl', () => {
- it('should get selected job id from the url', () => {
- const actual = getSelectedIdFromUrl(jobIdUrl);
- expect(actual).toStrictEqual({ jobId });
- });
-
- it('should get selected group ids from the url', () => {
- const expected = { groupIds: [groupIdOne, groupIdTwo] };
- const actual = getSelectedIdFromUrl(groupIdsUrl);
- expect(actual).toStrictEqual(expected);
- });
-
- it('should get selected group id from the url', () => {
- const expected = { groupIds: [groupIdOne] };
- const actual = getSelectedIdFromUrl(groupIdUrl);
- expect(actual).toStrictEqual(expected);
- });
- });
-});
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx
index 4c6469f6800a7..df50f53b811fa 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx
@@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { FC, useCallback, useMemo } from 'react';
+import React, { FC } from 'react';
import { NavigationMenu } from '../../components/navigation_menu';
// @ts-ignore
import { JobsListView } from './components/jobs_list_view/index';
-import { useUrlState } from '../../util/url_state';
+import { usePageUrlState } from '../../util/url_state';
+import { ML_PAGES } from '../../../../common/constants/ml_url_generator';
+import { ListingPageUrlState } from '../../../../common/types/common';
interface JobsPageProps {
blockRefresh?: boolean;
@@ -17,15 +19,7 @@ interface JobsPageProps {
lastRefresh?: number;
}
-export interface AnomalyDetectionJobsListState {
- pageSize: number;
- pageIndex: number;
- sortField: string;
- sortDirection: string;
- queryText?: string;
-}
-
-export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsListState => ({
+export const getDefaultAnomalyDetectionJobsListState = (): ListingPageUrlState => ({
pageIndex: 0,
pageSize: 10,
sortField: 'id',
@@ -33,33 +27,15 @@ export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsL
});
export const JobsPage: FC = (props) => {
- const [appState, setAppState] = useUrlState('_a');
-
- const jobListState: AnomalyDetectionJobsListState = useMemo(() => {
- return {
- ...getDefaultAnomalyDetectionJobsListState(),
- ...(appState ?? {}),
- };
- }, [appState]);
-
- const onJobsViewStateUpdate = useCallback(
- (update: Partial) => {
- setAppState({
- ...jobListState,
- ...update,
- });
- },
- [appState, setAppState]
+ const [pageState, setPageState] = usePageUrlState(
+ ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
+ getDefaultAnomalyDetectionJobsListState()
);
return (
-
+
);
};
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts
index 6671aaa83abe0..f23807f156576 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts
@@ -134,6 +134,7 @@ export const useModelMemoryEstimator = (
// Update model memory estimation payload on the job creator updates
useEffect(() => {
modelMemoryEstimator.update({
+ datafeedConfig: jobCreator.datafeedConfig,
analysisConfig: jobCreator.jobConfig.analysis_config,
indexPattern: jobCreator.indexPatternTitle,
query: jobCreator.datafeedConfig.query,
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts
index 635322a6c4469..1c012033e97c8 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts
@@ -10,6 +10,7 @@ import { map, startWith, tap } from 'rxjs/operators';
import {
basicJobValidation,
basicDatafeedValidation,
+ basicJobAndDatafeedValidation,
} from '../../../../../../common/util/job_utils';
import { getNewJobLimits } from '../../../../services/ml_server_info';
import { JobCreator, JobCreatorType, isCategorizationJobCreator } from '../job_creator';
@@ -53,6 +54,7 @@ export interface BasicValidations {
scrollSize: Validation;
categorizerMissingPerPartition: Validation;
categorizerVaryingPerPartitionField: Validation;
+ summaryCountField: Validation;
}
export interface AdvancedValidations {
@@ -80,6 +82,7 @@ export class JobValidator {
scrollSize: { valid: true },
categorizerMissingPerPartition: { valid: true },
categorizerVaryingPerPartitionField: { valid: true },
+ summaryCountField: { valid: true },
};
private _advancedValidations: AdvancedValidations = {
categorizationFieldValid: { valid: true },
@@ -197,6 +200,14 @@ export class JobValidator {
datafeedConfig
);
+ const basicJobAndDatafeedResults = basicJobAndDatafeedValidation(jobConfig, datafeedConfig);
+ populateValidationMessages(
+ basicJobAndDatafeedResults,
+ this._basicValidations,
+ jobConfig,
+ datafeedConfig
+ );
+
// run addition job and group id validation
const idResults = checkForExistingJobAndGroupIds(
this._jobCreator.jobId,
@@ -228,6 +239,9 @@ export class JobValidator {
public get bucketSpan(): Validation {
return this._basicValidations.bucketSpan;
}
+ public get summaryCountField(): Validation {
+ return this._basicValidations.summaryCountField;
+ }
public get duplicateDetectors(): Validation {
return this._basicValidations.duplicateDetectors;
@@ -297,6 +311,7 @@ export class JobValidator {
this.duplicateDetectors.valid &&
this.categorizerMissingPerPartition.valid &&
this.categorizerVaryingPerPartitionField.valid &&
+ this.summaryCountField.valid &&
!this.validating &&
(this._jobCreator.type !== JOB_TYPE.CATEGORIZATION ||
(this._jobCreator.type === JOB_TYPE.CATEGORIZATION && this.categorizationField))
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts
index 1ce81bf0dcdf0..04be935ed4399 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts
@@ -193,6 +193,15 @@ export function populateValidationMessages(
basicValidations.frequency.valid = false;
basicValidations.frequency.message = invalidTimeIntervalMessage(datafeedConfig.frequency);
}
+ if (validationResults.contains('missing_summary_count_field_name')) {
+ basicValidations.summaryCountField.valid = false;
+ basicValidations.summaryCountField.message = i18n.translate(
+ 'xpack.ml.newJob.wizard.validateJob.summaryCountFieldMissing',
+ {
+ defaultMessage: 'Required field as the datafeed uses aggregations.',
+ }
+ );
+ }
}
export function checkForExistingJobAndGroupIds(
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx
index 0dd802855ea67..cf98625672019 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx
@@ -61,9 +61,12 @@ export const DatafeedPreview: FC<{
if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) {
try {
const resp = await mlJobService.searchPreview(combinedJob);
- const data = resp.aggregations
- ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT)
- : resp.hits.hits;
+ let data = resp.hits.hits;
+ // the first item under aggregations can be any name
+ if (typeof resp.aggregations === 'object' && Object.keys(resp.aggregations).length > 0) {
+ const accessor = Object.keys(resp.aggregations)[0];
+ data = resp.aggregations[accessor].buckets.slice(0, ML_DATA_PREVIEW_COUNT);
+ }
setPreviewJsonString(JSON.stringify(data, null, 2));
} catch (error) {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx
index 5109718268ac3..a09b6540e101f 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx
@@ -7,23 +7,44 @@
import React, { memo, FC } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui';
+import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui';
+import { Validation } from '../../../../../common/job_validator';
+import { useMlKibana } from '../../../../../../../contexts/kibana';
-export const Description: FC = memo(({ children }) => {
+interface Props {
+ validation: Validation;
+}
+
+export const Description: FC = memo(({ children, validation }) => {
const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title', {
defaultMessage: 'Summary count field',
});
+ const {
+ services: { docLinks },
+ } = useMlKibana();
+ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
+ const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-aggregation.html`;
return (
{title}}
description={
+
+
+ ),
+ }}
/>
}
>
-
+
<>{children}>
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx
index af759117b8501..70eaa39f71c69 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx
@@ -17,13 +17,23 @@ import {
import { Description } from './description';
export const SummaryCountField: FC = () => {
- const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext);
+ const {
+ jobCreator: jc,
+ jobCreatorUpdate,
+ jobCreatorUpdated,
+ jobValidator,
+ jobValidatorUpdated,
+ } = useContext(JobCreatorContext);
const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator | AdvancedJobCreator;
const { fields } = newJobCapsService;
const [summaryCountFieldName, setSummaryCountFieldName] = useState(
jobCreator.summaryCountFieldName
);
+ const [validation, setValidation] = useState(jobValidator.summaryCountField);
+ useEffect(() => {
+ setValidation(jobValidator.summaryCountField);
+ }, [jobValidatorUpdated]);
useEffect(() => {
jobCreator.summaryCountFieldName = summaryCountFieldName;
@@ -35,7 +45,7 @@ export const SummaryCountField: FC = () => {
}, [jobCreatorUpdated]);
return (
-
+
(
- getDefaultAnomalyDetectionJobsListState()
- );
+function usePageState(
+ defaultState: T
+): [T, (update: Partial) => void] {
+ const [pageState, setPageState] = useState(defaultState);
const updateState = useCallback(
- (update: Partial) => {
- setJobsViewState({
- ...jobsViewState,
+ (update: Partial) => {
+ setPageState({
+ ...pageState,
...update,
});
},
- [jobsViewState]
+ [pageState]
);
+ return [pageState, updateState];
+}
+
+function useTabs(isMlEnabledInSpace: boolean): Tab[] {
+ const [adPageState, updateAdPageState] = usePageState(getDefaultAnomalyDetectionJobsListState());
+ const [dfaPageState, updateDfaPageState] = usePageState(getDefaultDFAListState());
+
return useMemo(
() => [
{
@@ -75,8 +81,8 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] {
@@ -95,12 +101,14 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] {
),
},
],
- [isMlEnabledInSpace, jobsViewState, updateState]
+ [isMlEnabledInSpace, adPageState, updateAdPageState, dfaPageState, updateDfaPageState]
);
}
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
index 80e93b2f1f5d9..b67b5015dbd6c 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
@@ -628,6 +628,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
},
calculateModelMemoryLimit$({
+ datafeedConfig,
analysisConfig,
indexPattern,
query,
@@ -635,6 +636,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
earliestMs,
latestMs,
}: {
+ datafeedConfig?: Datafeed;
analysisConfig: AnalysisConfig;
indexPattern: string;
query: any;
@@ -643,6 +645,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
latestMs: number;
}) {
const body = JSON.stringify({
+ datafeedConfig,
analysisConfig,
indexPattern,
query,
diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
index 2869a7439614f..79afe2ba5a0ad 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
@@ -16,9 +16,11 @@ import { map } from 'rxjs/operators';
import { each, get } from 'lodash';
import { Dictionary } from '../../../../common/types/common';
import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
-import { JobId } from '../../../../common/types/anomaly_detection_jobs';
+import { Datafeed, JobId } from '../../../../common/types/anomaly_detection_jobs';
import { MlApiServices } from '../ml_api_service';
import { CriteriaField } from './index';
+import { findAggField } from '../../../../common/util/validation_utils';
+import { getDatafeedAggregations } from '../../../../common/util/datafeed_utils';
import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils';
interface ResultResponse {
@@ -69,8 +71,12 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
timeFieldName: string,
earliestMs: number,
latestMs: number,
- intervalMs: number
+ intervalMs: number,
+ datafeedConfig?: Datafeed
): Observable {
+ const scriptFields = datafeedConfig?.script_fields;
+ const aggFields = getDatafeedAggregations(datafeedConfig);
+
// Build the criteria to use in the bool filter part of the request.
// Add criteria for the time range, entity fields,
// plus any additional supplied query.
@@ -151,15 +157,35 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
body.aggs.byTime.aggs = {};
const metricAgg: any = {
- [metricFunction]: {
- field: metricFieldName,
- },
+ [metricFunction]: {},
};
+ if (scriptFields !== undefined && scriptFields[metricFieldName] !== undefined) {
+ metricAgg[metricFunction].script = scriptFields[metricFieldName].script;
+ } else {
+ metricAgg[metricFunction].field = metricFieldName;
+ }
if (metricFunction === 'percentiles') {
metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS];
}
- body.aggs.byTime.aggs.metric = metricAgg;
+
+ // when the field is an aggregation field, because the field doesn't actually exist in the indices
+ // we need to pass all the sub aggs from the original datafeed config
+ // so that we can access the aggregated field
+ if (typeof aggFields === 'object' && Object.keys(aggFields).length > 0) {
+ // first item under aggregations can be any name, not necessarily 'buckets'
+ const accessor = Object.keys(aggFields)[0];
+ const tempAggs = { ...(aggFields[accessor].aggs ?? aggFields[accessor].aggregations) };
+ const foundValue = findAggField(tempAggs, metricFieldName);
+
+ if (foundValue !== undefined) {
+ tempAggs.metric = foundValue;
+ delete tempAggs[metricFieldName];
+ }
+ body.aggs.byTime.aggs = tempAggs;
+ } else {
+ body.aggs.byTime.aggs.metric = metricAgg;
+ }
}
return mlApiServices.esSearch$({ index, body }).pipe(
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
index d053d69b4d1f2..8419660a52a9a 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
@@ -286,7 +286,7 @@ export function resultsServiceProvider(mlApiServices) {
influencerFieldValues: {
terms: {
field: 'influencer_field_value',
- size: maxFieldValues,
+ size: !!maxFieldValues ? maxFieldValues : ANOMALY_SWIM_LANE_HARD_LIMIT,
order: {
maxAnomalyScore: 'desc',
},
@@ -416,7 +416,7 @@ export function resultsServiceProvider(mlApiServices) {
influencerFieldValues: {
terms: {
field: 'influencer_field_value',
- size: maxResults !== undefined ? maxResults : 2,
+ size: !!maxResults ? maxResults : 2,
order: {
maxAnomalyScore: 'desc',
},
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js
index 8df186c5c3c6e..b2d054becbb1a 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js
@@ -51,6 +51,7 @@ import {
unhighlightFocusChartAnnotation,
ANNOTATION_MIN_WIDTH,
} from './timeseries_chart_annotations';
+import { distinctUntilChanged } from 'rxjs/operators';
const focusZoomPanelHeight = 25;
const focusChartHeight = 310;
@@ -570,6 +571,7 @@ class TimeseriesChartIntl extends Component {
}
renderFocusChart() {
+ console.log('renderFocusChart');
const {
focusAggregationInterval,
focusAnnotationData: focusAnnotationDataOriginalPropValue,
@@ -1798,7 +1800,15 @@ class TimeseriesChartIntl extends Component {
}
export const TimeseriesChart = (props) => {
- const annotationProp = useObservable(annotation$);
+ const annotationProp = useObservable(
+ annotation$.pipe(
+ distinctUntilChanged((prev, curr) => {
+ // prevent re-rendering
+ return prev !== null && curr !== null;
+ })
+ )
+ );
+
if (annotationProp === undefined) {
return null;
}
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
index 0d7abdab90be0..90c39497a9a18 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
@@ -94,7 +94,8 @@ function getMetricData(
chartConfig.timeField,
earliestMs,
latestMs,
- intervalMs
+ intervalMs,
+ chartConfig?.datafeedConfig
)
.pipe(
map((resp) => {
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
index e3b6e38f47bab..f14f11e5d6149 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
@@ -1014,6 +1014,7 @@ export class TimeSeriesExplorer extends React.Component {
this.previousShowForecast = showForecast;
this.previousShowModelBounds = showModelBounds;
+ console.log('Timeseriesexplorer rerendered');
return (
{fieldNamesWithEmptyValues.length > 0 && (
diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx
index a3c70e1130904..448a888ab32c2 100644
--- a/x-pack/plugins/ml/public/application/util/url_state.tsx
+++ b/x-pack/plugins/ml/public/application/util/url_state.tsx
@@ -13,6 +13,7 @@ import { useHistory, useLocation } from 'react-router-dom';
import { Dictionary } from '../../../common/types/common';
import { getNestedProperty } from './object_utils';
+import { MlPages } from '../../../common/constants/ml_url_generator';
type Accessor = '_a' | '_g';
export type SetUrlState = (
@@ -150,3 +151,35 @@ export const useUrlState = (accessor: Accessor) => {
);
return [urlState, setUrlState];
};
+
+/**
+ * Hook for managing the URL state of the page.
+ */
+export const usePageUrlState = (
+ pageKey: MlPages,
+ defaultState: PageUrlState
+): [PageUrlState, (update: Partial) => void] => {
+ const [appState, setAppState] = useUrlState('_a');
+ const pageState = appState?.[pageKey];
+
+ const resultPageState: PageUrlState = useMemo(() => {
+ return {
+ ...defaultState,
+ ...(pageState ?? {}),
+ };
+ }, [pageState]);
+
+ const onStateUpdate = useCallback(
+ (update: Partial, replace?: boolean) => {
+ setAppState(pageKey, {
+ ...(replace ? {} : resultPageState),
+ ...update,
+ });
+ },
+ [pageKey, resultPageState, setAppState]
+ );
+
+ return useMemo(() => {
+ return [resultPageState, onStateUpdate];
+ }, [resultPageState, onStateUpdate]);
+};
diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts
index 717d293ccd7fa..6d7e286a29476 100644
--- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts
+++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts
@@ -19,8 +19,8 @@ import type {
import { ML_PAGES } from '../../common/constants/ml_url_generator';
import { createGenericMlUrl } from './common';
import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public';
-import type { AnomalyDetectionJobsListState } from '../application/jobs/jobs_list/jobs';
import { getGroupQueryText, getJobQueryText } from '../../common/util/string_utils';
+import { AppPageState, ListingPageUrlState } from '../../common/types/common';
/**
* Creates URL to the Anomaly Detection Job management page
*/
@@ -41,11 +41,15 @@ export function createAnomalyDetectionJobManagementUrl(
if (groupIds) {
queryTextArr.push(getGroupQueryText(groupIds));
}
- const queryState: Partial = {
+ const jobsListState: Partial = {
...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}),
};
- url = setStateToKbnUrl>(
+ const queryState: AppPageState = {
+ [ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE]: jobsListState,
+ };
+
+ url = setStateToKbnUrl>(
'_a',
queryState,
{ useHash: false, storeInHashQuery: false },
diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts
index 6c58a9d28bcc2..dc9c3bd86cc63 100644
--- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts
+++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts
@@ -10,12 +10,13 @@
import {
DataFrameAnalyticsExplorationQueryState,
DataFrameAnalyticsExplorationUrlState,
- DataFrameAnalyticsQueryState,
DataFrameAnalyticsUrlState,
MlCommonGlobalState,
} from '../../common/types/ml_url_generator';
import { ML_PAGES } from '../../common/constants/ml_url_generator';
import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public';
+import { getGroupQueryText, getJobQueryText } from '../../common/util/string_utils';
+import { AppPageState, ListingPageUrlState } from '../../common/types/common';
export function createDataFrameAnalyticsJobManagementUrl(
appBasePath: string,
@@ -26,13 +27,23 @@ export function createDataFrameAnalyticsJobManagementUrl(
if (mlUrlGeneratorState) {
const { jobId, groupIds, globalState } = mlUrlGeneratorState;
if (jobId || groupIds) {
- const queryState: Partial = {
- jobId,
- groupIds,
+ const queryTextArr = [];
+ if (jobId) {
+ queryTextArr.push(getJobQueryText(jobId));
+ }
+ if (groupIds) {
+ queryTextArr.push(getGroupQueryText(groupIds));
+ }
+ const jobsListState: Partial = {
+ ...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}),
+ };
+
+ const queryState: AppPageState = {
+ [ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE]: jobsListState,
};
- url = setStateToKbnUrl>(
- 'mlManagement',
+ url = setStateToKbnUrl>(
+ '_a',
queryState,
{ useHash: false, storeInHashQuery: false },
url
diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts
index e7f12ead3ffe9..3f3d88f1a31d9 100644
--- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts
+++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts
@@ -30,7 +30,7 @@ describe('MlUrlGenerator', () => {
jobId: 'fq_single_1',
},
});
- expect(url).toBe('/app/ml/jobs?_a=(queryText:fq_single_1)');
+ expect(url).toBe("/app/ml/jobs?_a=(jobs:(queryText:'id:fq_single_1'))");
});
it('should generate valid URL for the Anomaly Detection job management page for groupIds', async () => {
@@ -40,7 +40,9 @@ describe('MlUrlGenerator', () => {
groupIds: ['farequote', 'categorization'],
},
});
- expect(url).toBe("/app/ml/jobs?_a=(queryText:'groups:(farequote%20or%20categorization)')");
+ expect(url).toBe(
+ "/app/ml/jobs?_a=(jobs:(queryText:'groups:(farequote%20or%20categorization)'))"
+ );
});
it('should generate valid URL for the page for selecting the type of anomaly detection job to create', async () => {
@@ -180,7 +182,9 @@ describe('MlUrlGenerator', () => {
jobId: 'grid_regression_1',
},
});
- expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(jobId:grid_regression_1)');
+ expect(url).toBe(
+ "/app/ml/data_frame_analytics?_a=(data_frame_analytics:(queryText:'id:grid_regression_1'))"
+ );
});
it('should generate valid URL for the Data Frame Analytics job management page with groupIds', async () => {
@@ -190,7 +194,9 @@ describe('MlUrlGenerator', () => {
groupIds: ['group_1', 'group_2'],
},
});
- expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(groupIds:!(group_1,group_2))');
+ expect(url).toBe(
+ "/app/ml/data_frame_analytics?_a=(data_frame_analytics:(queryText:'groups:(group_1%20or%20group_2)'))"
+ );
});
});
diff --git a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts
index 180b4e71dfa9c..865f305f2ff9f 100644
--- a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts
+++ b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts
@@ -7,7 +7,7 @@
import numeral from '@elastic/numeral';
import { IScopedClusterClient } from 'kibana/server';
import { MLCATEGORY } from '../../../common/constants/field_types';
-import { AnalysisConfig } from '../../../common/types/anomaly_detection_jobs';
+import { AnalysisConfig, Datafeed } from '../../../common/types/anomaly_detection_jobs';
import { fieldsServiceProvider } from '../fields_service';
import { MlInfoResponse } from '../../../common/types/ml_server_info';
import type { MlClient } from '../../lib/ml_client';
@@ -46,7 +46,8 @@ const cardinalityCheckProvider = (client: IScopedClusterClient) => {
query: any,
timeFieldName: string,
earliestMs: number,
- latestMs: number
+ latestMs: number,
+ datafeedConfig?: Datafeed
): Promise<{
overallCardinality: { [key: string]: number };
maxBucketCardinality: { [key: string]: number };
@@ -101,7 +102,8 @@ const cardinalityCheckProvider = (client: IScopedClusterClient) => {
query,
timeFieldName,
earliestMs,
- latestMs
+ latestMs,
+ datafeedConfig
);
}
@@ -142,7 +144,8 @@ export function calculateModelMemoryLimitProvider(
timeFieldName: string,
earliestMs: number,
latestMs: number,
- allowMMLGreaterThanMax = false
+ allowMMLGreaterThanMax = false,
+ datafeedConfig?: Datafeed
): Promise {
const { body: info } = await mlClient.info();
const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase();
@@ -154,7 +157,8 @@ export function calculateModelMemoryLimitProvider(
query,
timeFieldName,
earliestMs,
- latestMs
+ latestMs,
+ datafeedConfig
);
const { body } = await mlClient.estimateModelMemory({
diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
index 1f59e990096a4..0142e44276eee 100644
--- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
+++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
@@ -15,6 +15,9 @@ import {
buildSamplerAggregation,
getSamplerAggregationsResponsePath,
} from '../../lib/query_utils';
+import { AggCardinality } from '../../../common/types/fields';
+import { getDatafeedAggregations } from '../../../common/util/datafeed_utils';
+import { Datafeed } from '../../../common/types/anomaly_detection_jobs';
const SAMPLER_TOP_TERMS_THRESHOLD = 100000;
const SAMPLER_TOP_TERMS_SHARD_SIZE = 5000;
@@ -121,12 +124,6 @@ interface AggHistogram {
};
}
-interface AggCardinality {
- cardinality: {
- field: string;
- };
-}
-
interface AggTerms {
terms: {
field: string;
@@ -597,23 +594,35 @@ export class DataVisualizer {
samplerShardSize: number,
timeFieldName: string,
earliestMs?: number,
- latestMs?: number
+ latestMs?: number,
+ datafeedConfig?: Datafeed
) {
const index = indexPatternTitle;
const size = 0;
const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query);
+ const datafeedAggregations = getDatafeedAggregations(datafeedConfig);
// Value count aggregation faster way of checking if field exists than using
// filter aggregation with exists query.
- const aggs: Aggs = {};
+ const aggs: Aggs = datafeedAggregations !== undefined ? { ...datafeedAggregations } : {};
+
aggregatableFields.forEach((field, i) => {
const safeFieldName = getSafeAggregationName(field, i);
aggs[`${safeFieldName}_count`] = {
filter: { exists: { field } },
};
- aggs[`${safeFieldName}_cardinality`] = {
- cardinality: { field },
- };
+
+ let cardinalityField: AggCardinality;
+ if (datafeedConfig?.script_fields?.hasOwnProperty(field)) {
+ cardinalityField = aggs[`${safeFieldName}_cardinality`] = {
+ cardinality: { script: datafeedConfig?.script_fields[field].script },
+ };
+ } else {
+ cardinalityField = {
+ cardinality: { field },
+ };
+ }
+ aggs[`${safeFieldName}_cardinality`] = cardinalityField;
});
const searchBody = {
@@ -661,10 +670,27 @@ export class DataVisualizer {
},
});
} else {
- stats.aggregatableNotExistsFields.push({
- fieldName: field,
- existsInDocs: false,
- });
+ if (datafeedConfig?.script_fields?.hasOwnProperty(field)) {
+ const cardinality = get(
+ aggregations,
+ [...aggsPath, `${safeFieldName}_cardinality`, 'value'],
+ 0
+ );
+ stats.aggregatableExistsFields.push({
+ fieldName: field,
+ existsInDocs: true,
+ stats: {
+ sampleCount,
+ count,
+ cardinality,
+ },
+ });
+ } else {
+ stats.aggregatableNotExistsFields.push({
+ fieldName: field,
+ existsInDocs: false,
+ });
+ }
}
});
diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts
index ed8d3f48e387c..17f35967a626d 100644
--- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts
+++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts
@@ -9,6 +9,10 @@ import { IScopedClusterClient } from 'kibana/server';
import { duration } from 'moment';
import { parseInterval } from '../../../common/util/parse_interval';
import { initCardinalityFieldsCache } from './fields_aggs_cache';
+import { AggCardinality } from '../../../common/types/fields';
+import { isValidAggregationField } from '../../../common/util/validation_utils';
+import { getDatafeedAggregations } from '../../../common/util/datafeed_utils';
+import { Datafeed } from '../../../common/types/anomaly_detection_jobs';
/**
* Service for carrying out queries to obtain data
@@ -35,14 +39,29 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) {
*/
async function getAggregatableFields(
index: string | string[],
- fieldNames: string[]
+ fieldNames: string[],
+ datafeedConfig?: Datafeed
): Promise {
const { body } = await asCurrentUser.fieldCaps({
index,
fields: fieldNames,
});
const aggregatableFields: string[] = [];
+ const datafeedAggregations = getDatafeedAggregations(datafeedConfig);
+
fieldNames.forEach((fieldName) => {
+ if (
+ typeof datafeedConfig?.script_fields === 'object' &&
+ datafeedConfig.script_fields.hasOwnProperty(fieldName)
+ ) {
+ aggregatableFields.push(fieldName);
+ }
+ if (
+ datafeedAggregations !== undefined &&
+ isValidAggregationField(datafeedAggregations, fieldName)
+ ) {
+ aggregatableFields.push(fieldName);
+ }
const fieldInfo = body.fields[fieldName];
const typeKeys = fieldInfo !== undefined ? Object.keys(fieldInfo) : [];
if (typeKeys.length > 0) {
@@ -67,10 +86,12 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) {
query: any,
timeFieldName: string,
earliestMs: number,
- latestMs: number
+ latestMs: number,
+ datafeedConfig?: Datafeed
): Promise<{ [key: string]: number }> {
- const aggregatableFields = await getAggregatableFields(index, fieldNames);
+ const aggregatableFields = await getAggregatableFields(index, fieldNames, datafeedConfig);
+ // getAggregatableFields doesn't account for scripted or aggregated fields
if (aggregatableFields.length === 0) {
return {};
}
@@ -112,10 +133,22 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) {
mustCriteria.push(query);
}
- const aggs = fieldsToAgg.reduce((obj, field) => {
- obj[field] = { cardinality: { field } };
- return obj;
- }, {} as { [field: string]: { cardinality: { field: string } } });
+ const aggs = fieldsToAgg.reduce(
+ (obj, field) => {
+ if (
+ typeof datafeedConfig?.script_fields === 'object' &&
+ datafeedConfig.script_fields.hasOwnProperty(field)
+ ) {
+ obj[field] = { cardinality: { script: datafeedConfig.script_fields[field].script } };
+ } else {
+ obj[field] = { cardinality: { field } };
+ }
+ return obj;
+ },
+ {} as {
+ [field: string]: AggCardinality;
+ }
+ );
const body = {
query: {
diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts
index e3fcc69596dc9..3526f9cebb89b 100644
--- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts
+++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts
@@ -27,6 +27,7 @@ import { validateTimeRange, isValidTimeField } from './validate_time_range';
import { validateJobSchema } from '../../routes/schemas/job_validation_schema';
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
import type { MlClient } from '../../lib/ml_client';
+import { getDatafeedAggregations } from '../../../common/util/datafeed_utils';
export type ValidateJobPayload = TypeOf;
@@ -100,6 +101,12 @@ export async function validateJob(
...(await validateModelMemoryLimit(client, mlClient, job, duration))
);
}
+
+ // if datafeed has aggregation, require job config to include a valid summary_doc_field_name
+ const datafeedAggregations = getDatafeedAggregations(job.datafeed_config);
+ if (datafeedAggregations !== undefined && !job.analysis_config?.summary_count_field_name) {
+ validationMessages.push({ id: 'missing_summary_count_field_name' });
+ }
} else {
validationMessages = basicValidation.messages;
validationMessages.push({ id: 'skipped_extended_tests' });
diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts
index c5822b863c83d..f2bcc6e50d86e 100644
--- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts
@@ -11,6 +11,8 @@ import { validateJobObject } from './validate_job_object';
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
import { Detector } from '../../../common/types/anomaly_detection_jobs';
import { MessageId, JobValidationMessage } from '../../../common/constants/messages';
+import { isValidAggregationField } from '../../../common/util/validation_utils';
+import { getDatafeedAggregations } from '../../../common/util/datafeed_utils';
function isValidCategorizationConfig(job: CombinedJob, fieldName: string): boolean {
return (
@@ -66,6 +68,7 @@ const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Valida
const relevantDetectors = detectors.filter((detector) => {
return typeof detector[fieldName] !== 'undefined';
});
+ const datafeedConfig = job.datafeed_config;
if (relevantDetectors.length > 0) {
try {
@@ -78,11 +81,26 @@ const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Valida
index: job.datafeed_config.indices.join(','),
fields: uniqueFieldNames,
});
+ const datafeedAggregations = getDatafeedAggregations(datafeedConfig);
let aggregatableFieldNames: string[] = [];
// parse fieldCaps to return an array of just the fields which are aggregatable
if (typeof fieldCaps === 'object' && typeof fieldCaps.fields === 'object') {
aggregatableFieldNames = uniqueFieldNames.filter((field) => {
+ if (
+ typeof datafeedConfig?.script_fields === 'object' &&
+ datafeedConfig?.script_fields.hasOwnProperty(field)
+ ) {
+ return true;
+ }
+ // if datafeed has aggregation fields, check recursively if field exist
+ if (
+ datafeedAggregations !== undefined &&
+ isValidAggregationField(datafeedAggregations, field)
+ ) {
+ return true;
+ }
+
if (typeof fieldCaps.fields[field] !== 'undefined') {
const fieldType = Object.keys(fieldCaps.fields[field])[0];
return fieldCaps.fields[field][fieldType].aggregatable;
@@ -96,7 +114,10 @@ const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Valida
job.datafeed_config.query,
aggregatableFieldNames,
0,
- job.data_description.time_field
+ job.data_description.time_field,
+ undefined,
+ undefined,
+ datafeedConfig
);
uniqueFieldNames.forEach((uniqueFieldName) => {
diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts
index 6721605355d96..f72885cf223fd 100644
--- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts
@@ -65,7 +65,8 @@ export async function validateModelMemoryLimit(
job.data_description.time_field,
duration!.start as number,
duration!.end as number,
- true
+ true,
+ job.datafeed_config
);
// @ts-expect-error
const mmlEstimateBytes: number = numeral(modelMemoryLimit).value();
diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts
index c11569b8bc1f3..769405c6ef7c2 100644
--- a/x-pack/plugins/ml/server/routes/job_validation.ts
+++ b/x-pack/plugins/ml/server/routes/job_validation.ts
@@ -7,7 +7,7 @@
import Boom from '@hapi/boom';
import { IScopedClusterClient } from 'kibana/server';
import { TypeOf } from '@kbn/config-schema';
-import { AnalysisConfig } from '../../common/types/anomaly_detection_jobs';
+import { AnalysisConfig, Datafeed } from '../../common/types/anomaly_detection_jobs';
import { wrapError } from '../client/error_wrapper';
import { RouteInitialization } from '../types';
import {
@@ -35,7 +35,15 @@ export function jobValidationRoutes(
mlClient: MlClient,
payload: CalculateModelMemoryLimitPayload
) {
- const { analysisConfig, indexPattern, query, timeFieldName, earliestMs, latestMs } = payload;
+ const {
+ datafeedConfig,
+ analysisConfig,
+ indexPattern,
+ query,
+ timeFieldName,
+ earliestMs,
+ latestMs,
+ } = payload;
return calculateModelMemoryLimitProvider(client, mlClient)(
analysisConfig as AnalysisConfig,
@@ -43,7 +51,9 @@ export function jobValidationRoutes(
query,
timeFieldName,
earliestMs,
- latestMs
+ latestMs,
+ undefined,
+ datafeedConfig as Datafeed
);
}
diff --git a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts
index ddfb49ce42cb8..f786607e70641 100644
--- a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts
@@ -20,6 +20,7 @@ export const estimateBucketSpanSchema = schema.object({
});
export const modelMemoryLimitSchema = schema.object({
+ datafeedConfig: datafeedConfigSchema,
analysisConfig: analysisConfigSchema,
indexPattern: schema.string(),
query: schema.any(),
diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx
index b0965f8708558..90ab5c2f888fe 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx
@@ -55,7 +55,9 @@ describe('JobsTableComponent', () => {
'[data-test-subj="jobs-table-link"]'
);
await waitFor(() =>
- expect(href).toEqual('/app/ml/jobs?_a=(queryText:linux_anomalous_network_activity_ecs)')
+ expect(href).toEqual(
+ "/app/ml/jobs?_a=(jobs:(queryText:'id:linux_anomalous_network_activity_ecs'))"
+ )
);
});
@@ -72,7 +74,7 @@ describe('JobsTableComponent', () => {
'[data-test-subj="jobs-table-link"]'
);
await waitFor(() =>
- expect(href).toEqual("/app/ml/jobs?_a=(queryText:'job%20id%20with%20spaces')")
+ expect(href).toEqual("/app/ml/jobs?_a=(jobs:(queryText:'id:job%20id%20with%20spaces'))")
);
});
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index ba6ac32f2e3d0..cd45a4f01fc64 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -11284,24 +11284,9 @@
"xpack.maps.layerWizardSelect.solutionsCategoryLabel": "ソリューション",
"xpack.maps.loadMap.errorAttemptingToLoadSavedMap": "マップを読み込めません",
"xpack.maps.map.initializeErrorTitle": "マップを初期化できません",
- "xpack.maps.mapListing.advancedSettingsLinkText": "高度な設定",
- "xpack.maps.mapListing.cancelTitle": "キャンセル",
- "xpack.maps.mapListing.createMapButtonLabel": "マップを作成",
- "xpack.maps.mapListing.deleteSelectedButtonLabel": "選択項目を削除",
- "xpack.maps.mapListing.deleteSelectedItemsTitle": "選択項目を削除しますか?",
- "xpack.maps.mapListing.deleteTitle": "削除",
- "xpack.maps.mapListing.deleteWarning": "削除されたアイテムは復元できません。",
"xpack.maps.mapListing.descriptionFieldTitle": "説明",
"xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "マップを読み込めません",
- "xpack.maps.mapListing.limitExceededTitle": "リスティング制限超過",
- "xpack.maps.mapListing.limitHelpDescription": "{totalItems} 個のアイテムがありますが、listingLimit の設定により {listingLimit} 個までしか下の表に表示できません。この設定は次の場所で変更できます ",
- "xpack.maps.mapListing.listingTableTitle": "マップ",
- "xpack.maps.mapListing.noItemsDescription": "マップがないようです。作成ボタンをクリックして作成してください。",
- "xpack.maps.mapListing.noMatchDescription": "検索に一致するアイテムがありません。",
- "xpack.maps.mapListing.searchAriaLabel": "フィルターアイテム",
- "xpack.maps.mapListing.searchPlaceholder": "検索…",
"xpack.maps.mapListing.titleFieldTitle": "タイトル",
- "xpack.maps.mapListing.unableToDeleteToastTitle": "マップを削除できません",
"xpack.maps.maps.choropleth.rightSourcePlaceholder": "インデックスパターンを選択",
"xpack.maps.mapSavedObjectLabel": "マップ",
"xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "自動的にマップをデータ境界に合わせる",
@@ -12191,8 +12176,6 @@
"xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state": "ステータス",
"xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.stats": "統計",
"xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettingsLabel": "ジョブの詳細",
- "xpack.ml.dataframe.analyticsList.experimentalBadgeLabel": "実験的",
- "xpack.ml.dataframe.analyticsList.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。",
"xpack.ml.dataframe.analyticsList.fetchSourceIndexPatternForCloneErrorMessage": "インデックスパターン{indexPattern}が存在するかどうかを確認するときにエラーが発生しました。{error}",
"xpack.ml.dataframe.analyticsList.forceStopModalBody": "{analyticsId}は失敗状態です。ジョブを停止して、エラーを修正する必要があります。",
"xpack.ml.dataframe.analyticsList.forceStopModalCancelButton": "キャンセル",
@@ -13205,7 +13188,6 @@
"xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsErrorCallout": "停止したパーティションのリストの取得中にエラーが発生しました。",
"xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsExistCallout": "パーティション単位の分類とstop_on_warn設定が有効です。ジョブ「{jobId}」の一部のパーティションは分類に適さず、さらなる分類または異常検知分析から除外されました。",
"xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsPreviewColumnName": "停止したパーティション名",
- "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.description": "オプション。インプットデータが事前にまとめられている場合に使用、例: \\{docCountParam\\}。",
"xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title": "サマリーカウントフィールド",
"xpack.ml.newJob.wizard.previewJsonButton": "JSON をプレビュー",
"xpack.ml.newJob.wizard.previousStepButton": "前へ",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 822ccf5cc8409..97396b09ca6c6 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -11297,24 +11297,9 @@
"xpack.maps.layerWizardSelect.solutionsCategoryLabel": "解决方案",
"xpack.maps.loadMap.errorAttemptingToLoadSavedMap": "无法加载地图",
"xpack.maps.map.initializeErrorTitle": "无法初始化地图",
- "xpack.maps.mapListing.advancedSettingsLinkText": "高级设置",
- "xpack.maps.mapListing.cancelTitle": "取消",
- "xpack.maps.mapListing.createMapButtonLabel": "创建地图",
- "xpack.maps.mapListing.deleteSelectedButtonLabel": "删除选定",
- "xpack.maps.mapListing.deleteSelectedItemsTitle": "删除选定项?",
- "xpack.maps.mapListing.deleteTitle": "删除",
- "xpack.maps.mapListing.deleteWarning": "您无法恢复已删除项。",
"xpack.maps.mapListing.descriptionFieldTitle": "描述",
"xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "无法加载地图",
- "xpack.maps.mapListing.limitExceededTitle": "已超过列表限制",
- "xpack.maps.mapListing.limitHelpDescription": "您有 {totalItems} 项,但您的 listingLimit 设置阻止下表显示 {listingLimit} 项以上。此设置可在以下选项下更改: ",
- "xpack.maps.mapListing.listingTableTitle": "Maps",
- "xpack.maps.mapListing.noItemsDescription": "似乎您没有任何地图。单击创建按钮来创建。",
- "xpack.maps.mapListing.noMatchDescription": "没有任何项匹配您的搜索。",
- "xpack.maps.mapListing.searchAriaLabel": "筛选项",
- "xpack.maps.mapListing.searchPlaceholder": "搜索......",
"xpack.maps.mapListing.titleFieldTitle": "标题",
- "xpack.maps.mapListing.unableToDeleteToastTitle": "无法删除地图",
"xpack.maps.maps.choropleth.rightSourcePlaceholder": "选择索引模式",
"xpack.maps.mapSavedObjectLabel": "地图",
"xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "使地图自适应数据边界",
@@ -12205,8 +12190,6 @@
"xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state": "状态",
"xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.stats": "统计",
"xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettingsLabel": "作业详情",
- "xpack.ml.dataframe.analyticsList.experimentalBadgeLabel": "实验性",
- "xpack.ml.dataframe.analyticsList.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。",
"xpack.ml.dataframe.analyticsList.fetchSourceIndexPatternForCloneErrorMessage": "检查索引模式 {indexPattern} 是否存在时发生错误:{error}",
"xpack.ml.dataframe.analyticsList.forceStopModalBody": "{analyticsId} 处于失败状态。您必须停止该作业并修复失败问题。",
"xpack.ml.dataframe.analyticsList.forceStopModalCancelButton": "取消",
@@ -13219,7 +13202,6 @@
"xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsErrorCallout": "提取已停止分区的列表时发生错误。",
"xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsExistCallout": "启用按分区分类和 stop_on_warn 设置。作业“{jobId}”中的某些分区不适合进行分类,已从进一步分类或异常检测分析中排除。",
"xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsPreviewColumnName": "已停止的分区名称",
- "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.description": "可选,用于输入数据已预汇总时,例如 \\{docCountParam\\}。",
"xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title": "汇总计数字段",
"xpack.ml.newJob.wizard.previewJsonButton": "预览 JSON",
"xpack.ml.newJob.wizard.previousStepButton": "上一页",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx
index e60785f70bffe..f5095101d96b5 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx
@@ -63,7 +63,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({
fullWidth
isInvalid={errors && errors.length > 0 && inputTargetValue !== undefined}
name={paramsProperty}
- value={inputTargetValue}
+ value={inputTargetValue || ''}
data-test-subj={`${paramsProperty}TextArea`}
onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)}
onFocus={(e: React.FocusEvent) => {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx
index fc05b237ccf5e..946bf064eb9ce 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx
@@ -50,7 +50,7 @@ export const TextFieldWithMessageVariables: React.FunctionComponent = ({
id={`${paramsProperty}Id`}
isInvalid={errors && errors.length > 0 && inputTargetValue !== undefined}
data-test-subj={`${paramsProperty}Input`}
- value={inputTargetValue}
+ value={inputTargetValue || ''}
onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)}
onFocus={(e: React.FocusEvent) => {
setCurrentTextElement(e.target);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx
index 00ff6fc132cdc..b53d0816ea068 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx
@@ -72,10 +72,8 @@ export const ConnectorAddFlyout = ({
const [isSaving, setIsSaving] = useState(false);
const closeFlyout = useCallback(() => {
- setActionType(undefined);
- setConnector(initialConnector);
onClose();
- }, [onClose, initialConnector]);
+ }, [onClose]);
const canSave = hasSaveActionsCapability(capabilities);
diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts
index ade78c31211ab..4cea7ddf4854a 100644
--- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts
+++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts
@@ -6,23 +6,19 @@
import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server';
import { SavedObjectReference } from '../../../../src/core/types';
-import { DynamicActionsState, SerializedEvent } from './types';
-import { AdvancedUiActionsServerPlugin } from './plugin';
+import { ActionFactory, DynamicActionsState, SerializedEvent } from './types';
import { SerializableState } from '../../../../src/plugins/kibana_utils/common';
export const dynamicActionEnhancement = (
- uiActionsEnhanced: AdvancedUiActionsServerPlugin
+ getActionFactory: (id: string) => undefined | ActionFactory
): EnhancementRegistryDefinition => {
return {
id: 'dynamicActions',
telemetry: (state: SerializableState, telemetry: Record) => {
let telemetryData = telemetry;
(state as DynamicActionsState).events.forEach((event: SerializedEvent) => {
- if (uiActionsEnhanced.getActionFactory(event.action.factoryId)) {
- telemetryData = uiActionsEnhanced
- .getActionFactory(event.action.factoryId)!
- .telemetry(event, telemetryData);
- }
+ const factory = getActionFactory(event.action.factoryId);
+ if (factory) telemetryData = factory.telemetry(event, telemetryData);
});
return telemetryData;
},
@@ -30,8 +26,9 @@ export const dynamicActionEnhancement = (
const references: SavedObjectReference[] = [];
const newState: DynamicActionsState = {
events: (state as DynamicActionsState).events.map((event: SerializedEvent) => {
- const result = uiActionsEnhanced.getActionFactory(event.action.factoryId)
- ? uiActionsEnhanced.getActionFactory(event.action.factoryId)!.extract(event)
+ const factory = getActionFactory(event.action.factoryId);
+ const result = factory
+ ? factory.extract(event)
: {
state: event,
references: [],
@@ -45,9 +42,8 @@ export const dynamicActionEnhancement = (
inject: (state: SerializableState, references: SavedObjectReference[]) => {
return {
events: (state as DynamicActionsState).events.map((event: SerializedEvent) => {
- return uiActionsEnhanced.getActionFactory(event.action.factoryId)
- ? uiActionsEnhanced.getActionFactory(event.action.factoryId)!.inject(event, references)
- : event;
+ const factory = getActionFactory(event.action.factoryId);
+ return factory ? factory.inject(event, references) : event;
}),
} as DynamicActionsState;
},
diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts
index 718304018730d..e6362418efc66 100644
--- a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts
+++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts
@@ -5,15 +5,10 @@
*/
import { identity } from 'lodash';
-import { CoreSetup, Plugin, SavedObjectReference } from '../../../../src/core/server';
+import { CoreSetup, Plugin } from '../../../../src/core/server';
import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server';
import { dynamicActionEnhancement } from './dynamic_action_enhancement';
-import {
- ActionFactoryRegistry,
- SerializedEvent,
- ActionFactoryDefinition,
- DynamicActionsState,
-} from './types';
+import { ActionFactoryRegistry, SerializedEvent, ActionFactoryDefinition } from './types';
export interface SetupContract {
registerActionFactory: (definition: ActionFactoryDefinition) => void;
@@ -32,7 +27,9 @@ export class AdvancedUiActionsServerPlugin
constructor() {}
public setup(core: CoreSetup, { embeddable }: SetupDependencies) {
- embeddable.registerEnhancement(dynamicActionEnhancement(this));
+ const getActionFactory = (actionFactoryId: string) => this.actionFactories.get(actionFactoryId);
+
+ embeddable.registerEnhancement(dynamicActionEnhancement(getActionFactory));
return {
registerActionFactory: this.registerActionFactory,
@@ -64,45 +61,4 @@ export class AdvancedUiActionsServerPlugin
migrations: definition.migrations || {},
});
};
-
- public readonly getActionFactory = (actionFactoryId: string) => {
- const actionFactory = this.actionFactories.get(actionFactoryId);
- return actionFactory;
- };
-
- public readonly telemetry = (state: DynamicActionsState, telemetry: Record = {}) => {
- state.events.forEach((event: SerializedEvent) => {
- if (this.actionFactories.has(event.action.factoryId)) {
- this.actionFactories.get(event.action.factoryId)!.telemetry(event, telemetry);
- }
- });
- return telemetry;
- };
-
- public readonly extract = (state: DynamicActionsState) => {
- const references: SavedObjectReference[] = [];
- const newState = {
- events: state.events.map((event: SerializedEvent) => {
- const result = this.actionFactories.has(event.action.factoryId)
- ? this.actionFactories.get(event.action.factoryId)!.extract(event)
- : {
- state: event,
- references: [],
- };
- result.references.forEach((r) => references.push(r));
- return result.state;
- }),
- };
- return { state: newState, references };
- };
-
- public readonly inject = (state: DynamicActionsState, references: SavedObjectReference[]) => {
- return {
- events: state.events.map((event: SerializedEvent) => {
- return this.actionFactories.has(event.action.factoryId)
- ? this.actionFactories.get(event.action.factoryId)!.inject(event, references)
- : event;
- }),
- };
- };
}
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts
index 86e5f2876ca28..427061b6c16d4 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts
@@ -7,8 +7,13 @@
import { getPingHistogram } from '../get_ping_histogram';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks';
+import * as intervalHelper from '../../helper/get_histogram_interval';
describe('getPingHistogram', () => {
+ beforeEach(() => {
+ jest.spyOn(intervalHelper, 'getHistogramInterval').mockReturnValue(36000);
+ });
+
const standardMockResponse: any = {
aggregations: {
timeseries: {
@@ -36,7 +41,7 @@ describe('getPingHistogram', () => {
},
};
- it.skip('returns a single bucket if array has 1', async () => {
+ it('returns a single bucket if array has 1', async () => {
expect.assertions(2);
const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts
index 020fcf5331188..2ff1043d79e84 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts
@@ -151,7 +151,7 @@ export const getHistogramForMonitors = async (
},
},
};
- const result = await queryContext.search(params);
+ const { body: result } = await queryContext.search(params);
const histoBuckets: any[] = result.aggregations?.histogram.buckets ?? [];
const simplified = histoBuckets.map((histoBucket: any): { timestamp: number; byId: any } => {
diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts
index f9fdfaed1c79b..cb78e76bdd697 100644
--- a/x-pack/test/alerting_api_integration/common/config.ts
+++ b/x-pack/test/alerting_api_integration/common/config.ts
@@ -92,6 +92,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
`--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`,
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
+ '--xpack.alerts.invalidateApiKeysTask.interval="15s"',
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
...actionsProxyUrl,
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts
index 7ed864afac4cc..998ec6ab2ed0e 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts
@@ -437,6 +437,21 @@ export function defineAlertTypes(
throw new Error('this alert is intended to fail');
},
};
+ const longRunningAlertType: AlertType = {
+ id: 'test.longRunning',
+ name: 'Test: Long Running',
+ actionGroups: [
+ {
+ id: 'default',
+ name: 'Default',
+ },
+ ],
+ producer: 'alertsFixture',
+ defaultActionGroupId: 'default',
+ async executor() {
+ await new Promise((resolve) => setTimeout(resolve, 5000));
+ },
+ };
alerts.registerType(getAlwaysFiringAlertType());
alerts.registerType(getCumulativeFiringAlertType());
@@ -449,4 +464,5 @@ export function defineAlertTypes(
alerts.registerType(onlyStateVariablesAlertType);
alerts.registerType(getPatternFiringAlertType());
alerts.registerType(throwAlertType);
+ alerts.registerType(longRunningAlertType);
}
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts
index fbf3b798500d3..d832902fe066d 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts
@@ -50,6 +50,7 @@ export class FixturePlugin implements Plugin,
+ res: KibanaResponseFactory
+ ): Promise> => {
+ try {
+ const [{ savedObjects }] = await core.getStartServices();
+ const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, {
+ includedHiddenTypes: ['api_key_pending_invalidation'],
+ });
+ const findResult = await savedObjectsWithTasksAndAlerts.find({
+ type: 'api_key_pending_invalidation',
+ });
+ return res.ok({
+ body: { apiKeysToInvalidate: findResult.saved_objects },
+ });
+ } catch (err) {
+ return res.badRequest({ body: err });
+ }
+ }
+ );
}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
index 8836bc2e4db2f..9c3d2801c0886 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
@@ -836,6 +836,80 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
}
});
+ it('should handle updates for a long running alert type without failing the underlying tasks due to invalidated ApiKey', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ enabled: true,
+ name: 'abc',
+ tags: ['foo'],
+ alertTypeId: 'test.longRunning',
+ consumer: 'alertsFixture',
+ schedule: { interval: '1s' },
+ throttle: '1m',
+ actions: [],
+ params: {},
+ })
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+ const updatedData = {
+ name: 'bcd',
+ tags: ['bar'],
+ params: {
+ foo: true,
+ },
+ schedule: { interval: '1m' },
+ actions: [],
+ throttle: '1m',
+ };
+ const response = await supertestWithoutAuth
+ .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send(updatedData);
+
+ const statusUpdates: string[] = [];
+ await retry.try(async () => {
+ const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0];
+ statusUpdates.push(alertTask.status);
+ expect(alertTask.status).to.eql('idle');
+ });
+
+ expect(statusUpdates.find((status) => status === 'failed')).to.be(undefined);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'update',
+ 'test.longRunning',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ await retry.try(async () => {
+ const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0];
+ expect(alertTask.status).to.eql('idle');
+ // ensure the alert is rescheduled to a minute from now
+ ensureDatetimeIsWithinRange(Date.parse(alertTask.runAt), 60 * 1000);
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
it('should handle updates to an alert schedule by setting the new schedule for the underlying task', async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts
index e4e6adca9640f..cb663115b958b 100644
--- a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts
+++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts
@@ -303,6 +303,12 @@ export default ({ getService }: FtrProviderContext) => {
url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#model-memory-limits`,
status: 'warning',
},
+ {
+ id: 'missing_summary_count_field_name',
+ status: 'error',
+ text:
+ 'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.',
+ },
];
expect(body.length).to.eql(
diff --git a/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap b/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap
new file mode 100644
index 0000000000000..50625683b605d
--- /dev/null
+++ b/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap
@@ -0,0 +1,369 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`monitor states endpoint will fetch monitor state data for the given down filters 1`] = `
+Object {
+ "nextPagePagination": "{\\"cursorDirection\\":\\"AFTER\\",\\"sortOrder\\":\\"ASC\\",\\"cursorKey\\":{\\"monitor_id\\":\\"0020-down\\"}}",
+ "prevPagePagination": null,
+ "summaries": Array [
+ Object {
+ "histogram": Object {
+ "points": Array [
+ Object {
+ "down": 1,
+ "timestamp": 1568172624744,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568172677247,
+ },
+ Object {
+ "down": 1,
+ "timestamp": 1568172729750,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568172782253,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568172834756,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568172887259,
+ },
+ Object {
+ "down": 1,
+ "timestamp": 1568172939762,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568172992265,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568173044768,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568173097271,
+ },
+ Object {
+ "down": 1,
+ "timestamp": 1568173149774,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568173202277,
+ },
+ ],
+ },
+ "minInterval": 52503,
+ "monitor_id": "0010-down",
+ "state": Object {
+ "monitor": Object {
+ "name": "",
+ },
+ "observer": Object {
+ "geo": Object {
+ "name": Array [
+ "mpls",
+ ],
+ },
+ },
+ "summary": Object {
+ "down": 1,
+ "status": "down",
+ "up": 0,
+ },
+ "summaryPings": Array [
+ Object {
+ "@timestamp": "2019-09-11T03:40:34.371Z",
+ "agent": Object {
+ "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85",
+ "hostname": "avc-x1x",
+ "id": "04e1d082-65bc-4929-8d65-d0768a2621c4",
+ "type": "heartbeat",
+ "version": "8.0.0",
+ },
+ "docId": "rZtoHm0B0I9WX_CznN_V",
+ "ecs": Object {
+ "version": "1.1.0",
+ },
+ "error": Object {
+ "message": "400 Bad Request",
+ "type": "validate",
+ },
+ "event": Object {
+ "dataset": "uptime",
+ },
+ "host": Object {
+ "name": "avc-x1x",
+ },
+ "http": Object {
+ "response": Object {
+ "body": Object {
+ "bytes": 3,
+ "content": "400",
+ "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94",
+ },
+ "status_code": 400,
+ },
+ "rtt": Object {
+ "content": Object {
+ "us": 41,
+ },
+ "response_header": Object {
+ "us": 36777,
+ },
+ "total": Object {
+ "us": 37821,
+ },
+ "validate": Object {
+ "us": 36818,
+ },
+ "write_request": Object {
+ "us": 53,
+ },
+ },
+ },
+ "monitor": Object {
+ "check_group": "d76f07d1-d445-11e9-88e3-3e80641b9c71",
+ "duration": Object {
+ "us": 37926,
+ },
+ "id": "0010-down",
+ "ip": "127.0.0.1",
+ "name": "",
+ "status": "down",
+ "type": "http",
+ },
+ "observer": Object {
+ "geo": Object {
+ "location": "37.926868, -78.024902",
+ "name": "mpls",
+ },
+ "hostname": "avc-x1x",
+ },
+ "resolve": Object {
+ "ip": "127.0.0.1",
+ "rtt": Object {
+ "us": 56,
+ },
+ },
+ "summary": Object {
+ "down": 1,
+ "up": 0,
+ },
+ "tcp": Object {
+ "rtt": Object {
+ "connect": Object {
+ "us": 890,
+ },
+ },
+ },
+ "timestamp": "2019-09-11T03:40:34.371Z",
+ "url": Object {
+ "domain": "localhost",
+ "full": "http://localhost:5678/pattern?r=400x1",
+ "path": "/pattern",
+ "port": 5678,
+ "query": "r=400x1",
+ "scheme": "http",
+ },
+ },
+ ],
+ "timestamp": "2019-09-11T03:40:34.371Z",
+ "url": Object {
+ "domain": "localhost",
+ "full": "http://localhost:5678/pattern?r=400x1",
+ "path": "/pattern",
+ "port": 5678,
+ "query": "r=400x1",
+ "scheme": "http",
+ },
+ },
+ },
+ Object {
+ "histogram": Object {
+ "points": Array [
+ Object {
+ "down": 1,
+ "timestamp": 1568172624744,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568172677247,
+ },
+ Object {
+ "down": 1,
+ "timestamp": 1568172729750,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568172782253,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568172834756,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568172887259,
+ },
+ Object {
+ "down": 1,
+ "timestamp": 1568172939762,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568172992265,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568173044768,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568173097271,
+ },
+ Object {
+ "down": 1,
+ "timestamp": 1568173149774,
+ },
+ Object {
+ "down": 2,
+ "timestamp": 1568173202277,
+ },
+ ],
+ },
+ "minInterval": 52503,
+ "monitor_id": "0020-down",
+ "state": Object {
+ "monitor": Object {
+ "name": "",
+ },
+ "observer": Object {
+ "geo": Object {
+ "name": Array [
+ "mpls",
+ ],
+ },
+ },
+ "summary": Object {
+ "down": 1,
+ "status": "down",
+ "up": 0,
+ },
+ "summaryPings": Array [
+ Object {
+ "@timestamp": "2019-09-11T03:40:34.372Z",
+ "agent": Object {
+ "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85",
+ "hostname": "avc-x1x",
+ "id": "04e1d082-65bc-4929-8d65-d0768a2621c4",
+ "type": "heartbeat",
+ "version": "8.0.0",
+ },
+ "docId": "X5toHm0B0I9WX_CznN-6",
+ "ecs": Object {
+ "version": "1.1.0",
+ },
+ "error": Object {
+ "message": "400 Bad Request",
+ "type": "validate",
+ },
+ "event": Object {
+ "dataset": "uptime",
+ },
+ "host": Object {
+ "name": "avc-x1x",
+ },
+ "http": Object {
+ "response": Object {
+ "body": Object {
+ "bytes": 3,
+ "content": "400",
+ "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94",
+ },
+ "status_code": 400,
+ },
+ "rtt": Object {
+ "content": Object {
+ "us": 54,
+ },
+ "response_header": Object {
+ "us": 180,
+ },
+ "total": Object {
+ "us": 555,
+ },
+ "validate": Object {
+ "us": 234,
+ },
+ "write_request": Object {
+ "us": 63,
+ },
+ },
+ },
+ "monitor": Object {
+ "check_group": "d7712ecb-d445-11e9-88e3-3e80641b9c71",
+ "duration": Object {
+ "us": 14900,
+ },
+ "id": "0020-down",
+ "ip": "127.0.0.1",
+ "name": "",
+ "status": "down",
+ "type": "http",
+ },
+ "observer": Object {
+ "geo": Object {
+ "location": "37.926868, -78.024902",
+ "name": "mpls",
+ },
+ "hostname": "avc-x1x",
+ },
+ "resolve": Object {
+ "ip": "127.0.0.1",
+ "rtt": Object {
+ "us": 14294,
+ },
+ },
+ "summary": Object {
+ "down": 1,
+ "up": 0,
+ },
+ "tcp": Object {
+ "rtt": Object {
+ "connect": Object {
+ "us": 105,
+ },
+ },
+ },
+ "timestamp": "2019-09-11T03:40:34.372Z",
+ "url": Object {
+ "domain": "localhost",
+ "full": "http://localhost:5678/pattern?r=400x1",
+ "path": "/pattern",
+ "port": 5678,
+ "query": "r=400x1",
+ "scheme": "http",
+ },
+ },
+ ],
+ "timestamp": "2019-09-11T03:40:34.372Z",
+ "url": Object {
+ "domain": "localhost",
+ "full": "http://localhost:5678/pattern?r=400x1",
+ "path": "/pattern",
+ "port": 5678,
+ "query": "r=400x1",
+ "scheme": "http",
+ },
+ },
+ },
+ ],
+ "totalSummaryCount": 2000,
+}
+`;
diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts
index f59b79a6b7bfc..6f410add0fa4d 100644
--- a/x-pack/test/api_integration/apis/uptime/rest/index.ts
+++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts
@@ -9,12 +9,15 @@ import {
settingsObjectId,
settingsObjectType,
} from '../../../../../plugins/uptime/server/lib/saved_objects';
+import { registerMochaHooksForSnapshots } from '../../../../apm_api_integration/common/match_snapshot';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const server = getService('kibanaServer');
describe('uptime REST endpoints', () => {
+ registerMochaHooksForSnapshots();
+
beforeEach('clear settings', async () => {
try {
await server.savedObjects.delete({
diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts
index d3c49bb49ff52..08a339ed59326 100644
--- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts
+++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts
@@ -9,6 +9,7 @@ import { isRight } from 'fp-ts/lib/Either';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { MonitorSummariesResultType } from '../../../../../plugins/uptime/common/runtime_types';
import { API_URLS } from '../../../../../plugins/uptime/common/constants';
+import { expectSnapshot } from '../../../../apm_api_integration/common/match_snapshot';
interface ExpectedMonitorStatesPage {
response: any;
@@ -90,6 +91,16 @@ export default function ({ getService }: FtrProviderContext) {
});
});
+ it('will fetch monitor state data for the given down filters', async () => {
+ const statusFilter = 'down';
+ const size = 2;
+ const { body } = await supertest.get(
+ `${API_URLS.MONITOR_LIST}?dateRangeStart=${from}&dateRangeEnd=${to}&statusFilter=${statusFilter}&pageSize=${size}`
+ );
+
+ expectSnapshot(body).toMatch();
+ });
+
it('can navigate forward and backward using pagination', async () => {
const expectedResultsCount = 100;
const size = 10;
diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js
index b5a7935a81eb5..6d86b93c3ec44 100644
--- a/x-pack/test/functional/apps/maps/mvt_super_fine.js
+++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js
@@ -19,7 +19,7 @@ export default function ({ getPageObjects, getService }) {
['global_maps_all', 'test_logstash_reader', 'geoshape_data_reader'],
false
);
- await PageObjects.maps.loadSavedMap('geo grid vector grid example (SUPER_FINE resolution)');
+ await PageObjects.maps.loadSavedMap('geo grid vector grid example SUPER_FINE resolution');
});
after(async () => {
diff --git a/x-pack/test/functional/apps/maps/saved_object_management.js b/x-pack/test/functional/apps/maps/saved_object_management.js
index 277a8a5651453..8c62136472921 100644
--- a/x-pack/test/functional/apps/maps/saved_object_management.js
+++ b/x-pack/test/functional/apps/maps/saved_object_management.js
@@ -139,8 +139,8 @@ export default function ({ getPageObjects, getService }) {
await PageObjects.maps.openNewMap();
await PageObjects.maps.saveMap(MAP1_NAME);
- const count = await PageObjects.maps.getMapCountWithName(MAP1_NAME);
- expect(count).to.equal(1);
+
+ await PageObjects.maps.searchAndExpectItemsCount(MAP1_NAME, 1);
});
it('should allow saving map that crosses dateline', async () => {
@@ -148,8 +148,8 @@ export default function ({ getPageObjects, getService }) {
await PageObjects.maps.setView('64', '179', '5');
await PageObjects.maps.saveMap(MAP2_NAME);
- const count = await PageObjects.maps.getMapCountWithName(MAP2_NAME);
- expect(count).to.equal(1);
+
+ await PageObjects.maps.searchAndExpectItemsCount(MAP2_NAME, 1);
});
});
@@ -157,11 +157,9 @@ export default function ({ getPageObjects, getService }) {
it('should delete selected saved objects', async () => {
await PageObjects.maps.deleteSavedMaps(MAP_NAME_PREFIX);
- const map1Count = await PageObjects.maps.getMapCountWithName(MAP1_NAME);
- expect(map1Count).to.equal(0);
+ await PageObjects.maps.searchAndExpectItemsCount(MAP1_NAME, 0);
- const map2Count = await PageObjects.maps.getMapCountWithName(MAP2_NAME);
- expect(map2Count).to.equal(0);
+ await PageObjects.maps.searchAndExpectItemsCount(MAP2_NAME, 0);
});
});
});
diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts
new file mode 100644
index 0000000000000..ff2bbd077fa8f
--- /dev/null
+++ b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts
@@ -0,0 +1,211 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { DeepPartial } from '../../../../../plugins/ml/common/types/common';
+import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common';
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const ml = getService('ml');
+
+ describe('total feature importance panel and decision path popover', function () {
+ const testDataList: Array<{
+ suiteTitle: string;
+ archive: string;
+ indexPattern: { name: string; timeField: string };
+ job: DeepPartial;
+ }> = (() => {
+ const timestamp = Date.now();
+
+ return [
+ {
+ suiteTitle: 'binary classification job',
+ archive: 'ml/ihp_outlier',
+ indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' },
+ job: {
+ id: `ihp_fi_binary_${timestamp}`,
+ description:
+ "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '35'",
+ source: {
+ index: ['ft_ihp_outlier'],
+ query: {
+ match_all: {},
+ },
+ },
+ dest: {
+ get index(): string {
+ return `user-ihp_fi_binary_${timestamp}`;
+ },
+ results_field: 'ml_central_air',
+ },
+ analyzed_fields: {
+ includes: [
+ 'CentralAir',
+ 'GarageArea',
+ 'GarageCars',
+ 'YearBuilt',
+ 'Electrical',
+ 'Neighborhood',
+ 'Heating',
+ '1stFlrSF',
+ ],
+ },
+ analysis: {
+ classification: {
+ dependent_variable: 'CentralAir',
+ num_top_feature_importance_values: 5,
+ training_percent: 35,
+ prediction_field_name: 'CentralAir_prediction',
+ num_top_classes: -1,
+ },
+ },
+ model_memory_limit: '60mb',
+ allow_lazy_start: false,
+ },
+ },
+ {
+ suiteTitle: 'multi class classification job',
+ archive: 'ml/ihp_outlier',
+ indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' },
+ job: {
+ id: `ihp_fi_multi_${timestamp}`,
+ description:
+ "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '35'",
+ source: {
+ index: ['ft_ihp_outlier'],
+ query: {
+ match_all: {},
+ },
+ },
+ dest: {
+ get index(): string {
+ return `user-ihp_fi_multi_${timestamp}`;
+ },
+ results_field: 'ml_heating_qc',
+ },
+ analyzed_fields: {
+ includes: [
+ 'CentralAir',
+ 'GarageArea',
+ 'GarageCars',
+ 'Electrical',
+ 'Neighborhood',
+ 'Heating',
+ '1stFlrSF',
+ 'HeatingQC',
+ ],
+ },
+ analysis: {
+ classification: {
+ dependent_variable: 'HeatingQC',
+ num_top_feature_importance_values: 5,
+ training_percent: 35,
+ prediction_field_name: 'heatingqc',
+ num_top_classes: -1,
+ },
+ },
+ model_memory_limit: '60mb',
+ allow_lazy_start: false,
+ },
+ },
+ {
+ suiteTitle: 'regression job',
+ archive: 'ml/egs_regression',
+ indexPattern: { name: 'ft_egs_regression', timeField: '@timestamp' },
+ job: {
+ id: `egs_fi_reg_${timestamp}`,
+ description: 'This is the job description',
+ source: {
+ index: ['ft_egs_regression'],
+ query: {
+ match_all: {},
+ },
+ },
+ dest: {
+ get index(): string {
+ return `user-egs_fi_reg_${timestamp}`;
+ },
+ results_field: 'ml',
+ },
+ analysis: {
+ regression: {
+ prediction_field_name: 'test',
+ dependent_variable: 'stab',
+ num_top_feature_importance_values: 5,
+ training_percent: 35,
+ },
+ },
+ analyzed_fields: {
+ includes: [
+ 'g1',
+ 'g2',
+ 'g3',
+ 'g4',
+ 'p1',
+ 'p2',
+ 'p3',
+ 'p4',
+ 'stab',
+ 'tau1',
+ 'tau2',
+ 'tau3',
+ 'tau4',
+ ],
+ excludes: [],
+ },
+ model_memory_limit: '20mb',
+ },
+ },
+ ];
+ })();
+
+ before(async () => {
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ await ml.securityUI.loginAsMlPowerUser();
+ for (const testData of testDataList) {
+ await esArchiver.loadIfNeeded(testData.archive);
+ await ml.testResources.createIndexPatternIfNeeded(
+ testData.indexPattern.name,
+ testData.indexPattern.timeField
+ );
+ await ml.api.createAndRunDFAJob(testData.job as DataFrameAnalyticsConfig);
+ }
+ });
+
+ after(async () => {
+ await ml.api.cleanMlIndices();
+ });
+
+ for (const testData of testDataList) {
+ describe(`${testData.suiteTitle}`, function () {
+ before(async () => {
+ await ml.navigation.navigateToMl();
+ await ml.navigation.navigateToDataFrameAnalytics();
+ await ml.dataFrameAnalyticsTable.waitForAnalyticsToLoad();
+ await ml.dataFrameAnalyticsTable.openResultsView(testData.job.id as string);
+ });
+
+ after(async () => {
+ await ml.api.deleteIndices(testData.job.dest!.index as string);
+ await ml.testResources.deleteIndexPatternByTitle(testData.job.dest!.index as string);
+ });
+
+ it('should display the total feature importance in the results view', async () => {
+ await ml.dataFrameAnalyticsResults.assertTotalFeatureImportanceEvaluatePanelExists();
+ });
+
+ it('should display the feature importance decision path in the data grid', async () => {
+ await ml.dataFrameAnalyticsResults.assertResultsTableExists();
+ await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty();
+ await ml.dataFrameAnalyticsResults.openFeatureImportanceDecisionPathPopover();
+ await ml.dataFrameAnalyticsResults.assertFeatureImportanceDecisionPathElementsExists();
+ await ml.dataFrameAnalyticsResults.assertFeatureImportanceDecisionPathChartElementsExists();
+ });
+ });
+ }
+ });
+}
diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts
index 0202c8431ce34..a57d26b536b4f 100644
--- a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts
+++ b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts
@@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./regression_creation'));
loadTestFile(require.resolve('./classification_creation'));
loadTestFile(require.resolve('./cloning'));
+ loadTestFile(require.resolve('./feature_importance'));
});
}
diff --git a/x-pack/test/functional/es_archives/discover/default/mappings.json b/x-pack/test/functional/es_archives/discover/default/mappings.json
index 82002c095bcc5..53bbe8a5baa5b 100644
--- a/x-pack/test/functional/es_archives/discover/default/mappings.json
+++ b/x-pack/test/functional/es_archives/discover/default/mappings.json
@@ -93,6 +93,9 @@
},
"title": {
"type": "text"
+ },
+ "fieldAttrs": {
+ "type": "text"
}
}
},
diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json
index e3a8743e60897..79e8c14cc3982 100644
--- a/x-pack/test/functional/es_archives/maps/kibana/data.json
+++ b/x-pack/test/functional/es_archives/maps/kibana/data.json
@@ -638,7 +638,7 @@
"description": "",
"layerListJSON": "[{\"id\":\"g1xkv\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"resolution\": \"SUPER_FINE\",\"type\":\"ES_GEO_GRID\",\"id\":\"9305f6ea-4518-4c06-95b9-33321aa38d6a\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"requestType\":\"grid\",\"metrics\":[{\"type\":\"count\"},{\"type\":\"max\",\"field\":\"bytes\"}]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max of bytes\",\"name\":\"max_of_bytes\",\"origin\":\"source\"},\"color\":\"Blues\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#cccccc\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"TILED_VECTOR\"}]",
"mapStateJSON": "{\"zoom\":3.59,\"center\":{\"lon\":-98.05765,\"lat\":38.32288},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}",
- "title": "geo grid vector grid example (SUPER_FINE resolution)",
+ "title": "geo grid vector grid example SUPER_FINE resolution",
"uiStateJSON": "{\"isDarkMode\":false}"
},
"type": "map"
diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts
index c4f1bd7dc2a6b..7e22acf785d36 100644
--- a/x-pack/test/functional/page_objects/gis_page.ts
+++ b/x-pack/test/functional/page_objects/gis_page.ts
@@ -21,6 +21,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
const renderable = getService('renderable');
const browser = getService('browser');
const MenuToggle = getService('MenuToggle');
+ const listingTable = getService('listingTable');
const setViewPopoverToggle = new MenuToggle({
name: 'SetView Popover',
@@ -120,13 +121,10 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
await retry.try(async () => {
await this.searchForMapWithName(name);
- await this.selectMap(name);
+ await listingTable.clickItemLink('map', name);
await PageObjects.header.waitUntilLoadingHasFinished();
-
- const onMapListingPage = await this.onMapListingPage();
- if (onMapListingPage) {
- throw new Error(`Failed to open map ${name}`);
- }
+ // check Map landing page is not present
+ await testSubjects.missingOrFail('mapLandingPage', { timeout: 10000 });
});
await this.waitForLayersToLoad();
@@ -134,8 +132,8 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
async deleteSavedMaps(search: string) {
await this.searchForMapWithName(search);
- await testSubjects.click('checkboxSelectAll');
- await testSubjects.click('deleteSelectedItems');
+ await listingTable.checkListingSelectAllCheckbox();
+ await listingTable.clickDeleteSelected();
await PageObjects.common.clickConfirmOnModal();
await PageObjects.header.waitUntilLoadingHasFinished();
@@ -150,7 +148,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
await renderable.waitForRender();
}
- async saveMap(name: string, uncheckReturnToOriginModeSwitch = false) {
+ async saveMap(name: string, uncheckReturnToOriginModeSwitch = false, tags?: string[]) {
await testSubjects.click('mapSaveButton');
await testSubjects.setValue('savedObjectTitle', name);
if (uncheckReturnToOriginModeSwitch) {
@@ -162,6 +160,13 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
}
await testSubjects.setEuiSwitch('returnToOriginModeSwitch', 'uncheck');
}
+ if (tags) {
+ await testSubjects.click('savedObjectTagSelector');
+ for (const tagName of tags) {
+ await testSubjects.click(`tagSelectorOption-${tagName.replace(' ', '_')}`);
+ }
+ await testSubjects.click('savedObjectTitle');
+ }
await testSubjects.clickWhenNotDisabled('confirmSaveSavedObjectButton');
}
@@ -174,7 +179,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
}
async expectMissingCreateNewButton() {
- await testSubjects.missingOrFail('newMapLink');
+ await testSubjects.missingOrFail('newItemButton');
}
async expectMissingAddLayerButton() {
@@ -187,8 +192,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
async onMapListingPage() {
log.debug(`onMapListingPage`);
- const exists = await testSubjects.exists('mapsListingPage', { timeout: 3500 });
- return exists;
+ return await listingTable.onListingPage('map');
}
async searchForMapWithName(name: string) {
@@ -196,21 +200,11 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
await this.gotoMapListingPage();
- await retry.try(async () => {
- const searchFilter = await testSubjects.find('searchFilter');
- await searchFilter.clearValue();
- await searchFilter.click();
- await searchFilter.type(name);
- await PageObjects.common.pressEnterKey();
- });
+ await listingTable.searchForItemWithName(name);
await PageObjects.header.waitUntilLoadingHasFinished();
}
- async selectMap(name: string) {
- await testSubjects.click(`mapListingTitleLink-${name.split(' ').join('-')}`);
- }
-
async getHits() {
await inspector.open();
await inspector.openInspectorRequestsView();
@@ -232,13 +226,11 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte
}
}
- async getMapCountWithName(name: string) {
+ async searchAndExpectItemsCount(name: string, count: number) {
await this.gotoMapListingPage();
- log.debug(`getMapCountWithName: ${name}`);
- await this.searchForMapWithName(name);
- const buttons = await find.allByButtonText(name);
- return buttons.length;
+ log.debug(`searchAndExpectItemsCount: ${name}`);
+ await listingTable.searchAndExpectItemsCount('map', name, count);
}
async setView(lat: number, lon: number, zoom: number) {
diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts
index 8781a2cd216f2..1ac11a0149897 100644
--- a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts
+++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts
@@ -5,12 +5,14 @@
*/
import expect from '@kbn/expect';
+import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper';
import { FtrProviderContext } from '../../ftr_provider_context';
export function MachineLearningDataFrameAnalyticsResultsProvider({
getService,
}: FtrProviderContext) {
+ const retry = getService('retry');
const testSubjects = getService('testSubjects');
return {
@@ -60,5 +62,59 @@ export function MachineLearningDataFrameAnalyticsResultsProvider({
`DFA results table should have at least one row (got '${resultTableRows.length}')`
);
},
+
+ async assertTotalFeatureImportanceEvaluatePanelExists() {
+ await testSubjects.existOrFail('mlDFExpandableSection-FeatureImportanceSummary');
+ await testSubjects.existOrFail('mlTotalFeatureImportanceChart', { timeout: 5000 });
+ },
+
+ async assertFeatureImportanceDecisionPathElementsExists() {
+ await testSubjects.existOrFail('mlDFADecisionPathPopoverTab-decision_path_chart', {
+ timeout: 5000,
+ });
+ await testSubjects.existOrFail('mlDFADecisionPathPopoverTab-decision_path_json', {
+ timeout: 5000,
+ });
+ },
+
+ async assertFeatureImportanceDecisionPathChartElementsExists() {
+ await testSubjects.existOrFail('mlDFADecisionPathChart', {
+ timeout: 5000,
+ });
+ },
+
+ async openFeatureImportanceDecisionPathPopover() {
+ this.assertResultsTableNotEmpty();
+
+ const featureImportanceCell = await this.getFirstFeatureImportanceCell();
+ const interactionButton = await featureImportanceCell.findByTagName('button');
+
+ // simulate hover and wait for button to appear
+ await featureImportanceCell.moveMouseTo();
+ await this.waitForInteractionButtonToDisplay(interactionButton);
+
+ // open popover
+ await interactionButton.click();
+ await testSubjects.existOrFail('mlDFADecisionPathPopover');
+ },
+
+ async getFirstFeatureImportanceCell(): Promise {
+ // get first row of the data grid
+ const firstDataGridRow = await testSubjects.find(
+ 'mlExplorationDataGrid loaded > dataGridRow'
+ );
+ // find the feature importance cell in that row
+ const featureImportanceCell = await firstDataGridRow.findByCssSelector(
+ '[data-test-subj="dataGridRowCell"][class*="featureImportance"]'
+ );
+ return featureImportanceCell;
+ },
+
+ async waitForInteractionButtonToDisplay(interactionButton: WebElementWrapper) {
+ await retry.tryForTime(5000, async () => {
+ const buttonVisible = await interactionButton.isDisplayed();
+ expect(buttonVisible).to.equal(true, 'Expected data grid cell button to be visible');
+ });
+ },
};
}
diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts
index 4f08134365e95..8734b7cf5bb68 100644
--- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts
+++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts
@@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) {
USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER,
USERS.DEFAULT_SPACE_DASHBOARD_READ_USER,
USERS.DEFAULT_SPACE_VISUALIZE_READ_USER,
+ USERS.DEFAULT_SPACE_MAPS_READ_USER,
],
unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER],
};
diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts
index 70884ba6c968b..8ca92ac472c6e 100644
--- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts
+++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts
@@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) {
USERS.DEFAULT_SPACE_DASHBOARD_READ_USER,
USERS.DEFAULT_SPACE_VISUALIZE_READ_USER,
USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER,
+ USERS.DEFAULT_SPACE_MAPS_READ_USER,
USERS.NOT_A_KIBANA_USER,
],
};
diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts
index 64f120fd75629..a2e3630622d67 100644
--- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts
+++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts
@@ -53,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) {
USERS.DEFAULT_SPACE_DASHBOARD_READ_USER,
USERS.DEFAULT_SPACE_VISUALIZE_READ_USER,
USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER,
+ USERS.DEFAULT_SPACE_MAPS_READ_USER,
USERS.NOT_A_KIBANA_USER,
],
};
diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts
index 1a354bbbcb660..9cde766b4f514 100644
--- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts
+++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts
@@ -57,6 +57,7 @@ export default function ({ getService }: FtrProviderContext) {
USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER,
USERS.DEFAULT_SPACE_DASHBOARD_READ_USER,
USERS.DEFAULT_SPACE_VISUALIZE_READ_USER,
+ USERS.DEFAULT_SPACE_MAPS_READ_USER,
],
unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER],
};
diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts
index 61b859cf81992..677bdee56ed8b 100644
--- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts
+++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts
@@ -63,6 +63,7 @@ export default function ({ getService }: FtrProviderContext) {
USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER,
USERS.DEFAULT_SPACE_DASHBOARD_READ_USER,
USERS.DEFAULT_SPACE_VISUALIZE_READ_USER,
+ USERS.DEFAULT_SPACE_MAPS_READ_USER,
],
unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER],
};
diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts
index 77bf9d7ca3287..3347eca9920d6 100644
--- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts
+++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts
@@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) {
USERS.DEFAULT_SPACE_DASHBOARD_READ_USER,
USERS.DEFAULT_SPACE_VISUALIZE_READ_USER,
USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER,
+ USERS.DEFAULT_SPACE_MAPS_READ_USER,
USERS.NOT_A_KIBANA_USER,
],
};
diff --git a/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json
new file mode 100644
index 0000000000000..cdaf4fe171ec0
--- /dev/null
+++ b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json
@@ -0,0 +1,210 @@
+{
+ "type": "doc",
+ "value": {
+ "id": "space:default",
+ "index": ".kibana",
+ "source": {
+ "space": {
+ "_reserved": true,
+ "description": "This is the default space",
+ "name": "Default Space"
+ },
+ "type": "space",
+ "updated_at": "2017-09-21T18:49:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "tag:tag-1",
+ "index": ".kibana",
+ "source": {
+ "tag": {
+ "name": "tag-1",
+ "description": "My first tag!",
+ "color": "#FF00FF"
+ },
+ "type": "tag",
+ "updated_at": "2017-09-21T18:49:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "tag:tag-2",
+ "index": ".kibana",
+ "source": {
+ "tag": {
+ "name": "tag-2",
+ "description": "Another awesome tag",
+ "color": "#11FF22"
+ },
+ "type": "tag",
+ "updated_at": "2017-09-21T18:49:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "tag:tag-3",
+ "index": ".kibana",
+ "source": {
+ "tag": {
+ "name": "tag-3",
+ "description": "Last but not least",
+ "color": "#AA0077"
+ },
+ "type": "tag",
+ "updated_at": "2017-09-21T18:49:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "config:6.3.0",
+ "index": ".kibana",
+ "source": {
+ "config": {
+ "buildNum": 8467,
+ "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c"
+ },
+ "references": [
+ ],
+ "type": "config",
+ "updated_at": "2018-04-11T20:43:55.434Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "map:63af0ed0-2515-11eb-8f44-eda8d4b698b3",
+ "index": ".kibana",
+ "source": {
+ "map": {
+ "title" : "map 3 (tag-1 and tag-3)",
+ "description" : "",
+ "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"d897b506-e719-42b8-9927-351eedd7d357\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]",
+ "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
+ "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"
+ },
+ "type" : "map",
+ "references" : [
+ {
+ "type" : "tag",
+ "id" : "tag-1",
+ "name" : "tag-ref-tag-1"
+ },
+ {
+ "type" : "tag",
+ "id" : "tag-3",
+ "name" : "tag-ref-tag-3"
+ }
+ ],
+ "migrationVersion" : {
+ "map" : "7.10.0"
+ },
+ "updated_at" : "2020-11-12T18:32:16.189Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "map:4afc6d10-2515-11eb-8f44-eda8d4b698b3",
+ "index": ".kibana",
+ "source": {
+ "map" : {
+ "title" : "map 1 (tag-2)",
+ "description" : "",
+ "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"6a91fb66-465c-4193-8c59-9b3f5f262756\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]",
+ "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":16.22097},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
+ "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"
+ },
+ "type" : "map",
+ "references" : [
+ {
+ "type" : "tag",
+ "id" : "tag-2",
+ "name" : "tag-ref-tag-2"
+ }
+ ],
+ "migrationVersion" : {
+ "map" : "7.10.0"
+ },
+ "updated_at" : "2020-11-12T18:31:34.753Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "map:562cce50-2515-11eb-8f44-eda8d4b698b3",
+ "index": ".kibana",
+ "source": {
+ "map" : {
+ "title" : "map 2 (tag-3)",
+ "description" : "",
+ "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"285d5190-aaf1-4dfc-912b-9c7d9e0104a8\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]",
+ "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
+ "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"
+ },
+ "type" : "map",
+ "references" : [
+ {
+ "type" : "tag",
+ "id" : "tag-3",
+ "name" : "tag-ref-tag-3"
+ }
+ ],
+ "migrationVersion" : {
+ "map" : "7.10.0"
+ },
+ "updated_at" : "2020-11-12T18:31:53.525Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "map:6f021340-2515-11eb-8f44-eda8d4b698b3",
+ "index": ".kibana",
+ "source": {
+ "map" : {
+ "title" : "map 4 (tag-1)",
+ "description" : "",
+ "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"3deeb666-33cf-4e9a-ab78-e453ed9d721d\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]",
+ "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
+ "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"
+ },
+ "type" : "map",
+ "references" : [
+ {
+ "type" : "tag",
+ "id" : "tag-1",
+ "name" : "tag-ref-tag-1"
+ }
+ ],
+ "migrationVersion" : {
+ "map" : "7.10.0"
+ },
+ "updated_at" : "2020-11-12T18:32:35.188Z"
+ }
+ }
+}
diff --git a/x-pack/test/saved_object_tagging/common/lib/authentication.ts b/x-pack/test/saved_object_tagging/common/lib/authentication.ts
index c318755bedcdd..8917057ad685e 100644
--- a/x-pack/test/saved_object_tagging/common/lib/authentication.ts
+++ b/x-pack/test/saved_object_tagging/common/lib/authentication.ts
@@ -118,6 +118,19 @@ export const ROLES = {
],
},
},
+ KIBANA_RBAC_DEFAULT_SPACE_MAPS_READ_USER: {
+ name: 'kibana_rbac_default_space_maps_read_user',
+ privileges: {
+ kibana: [
+ {
+ feature: {
+ maps: ['read'],
+ },
+ spaces: ['default'],
+ },
+ ],
+ },
+ },
};
export const USERS = {
@@ -185,4 +198,9 @@ export const USERS = {
password: 'password',
roles: [ROLES.KIBANA_RBAC_DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER.name],
},
+ DEFAULT_SPACE_MAPS_READ_USER: {
+ username: 'a_kibana_rbac_default_space_maps_read_user',
+ password: 'password',
+ roles: [ROLES.KIBANA_RBAC_DEFAULT_SPACE_MAPS_READ_USER.name],
+ },
};
diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts
index 43673487ba74f..0ddfa64d682a8 100644
--- a/x-pack/test/saved_object_tagging/functional/tests/index.ts
+++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts
@@ -23,5 +23,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) {
loadTestFile(require.resolve('./visualize_integration'));
loadTestFile(require.resolve('./dashboard_integration'));
loadTestFile(require.resolve('./feature_control'));
+ loadTestFile(require.resolve('./maps_integration'));
});
}
diff --git a/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts
new file mode 100644
index 0000000000000..4e44659b4fc67
--- /dev/null
+++ b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts
@@ -0,0 +1,139 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../ftr_provider_context';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getPageObjects, getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const listingTable = getService('listingTable');
+ const testSubjects = getService('testSubjects');
+ const find = getService('find');
+ const PageObjects = getPageObjects(['maps', 'tagManagement', 'common']);
+
+ /**
+ * Select tags in the searchbar's tag filter.
+ */
+ const selectFilterTags = async (...tagNames: string[]) => {
+ // open the filter dropdown
+ const filterButton = await find.byCssSelector('.euiFilterGroup .euiFilterButton');
+ await filterButton.click();
+ // select the tags
+ for (const tagName of tagNames) {
+ await testSubjects.click(
+ `tag-searchbar-option-${PageObjects.tagManagement.testSubjFriendly(tagName)}`
+ );
+ }
+ // click elsewhere to close the filter dropdown
+ const searchFilter = await find.byCssSelector('main .euiFieldSearch');
+ await searchFilter.click();
+ };
+
+ describe('maps integration', () => {
+ before(async () => {
+ await esArchiver.load('maps');
+ });
+ after(async () => {
+ await esArchiver.unload('maps');
+ });
+
+ describe('listing', () => {
+ beforeEach(async () => {
+ await PageObjects.common.navigateToUrlWithBrowserHistory('maps', '/');
+ await PageObjects.maps.gotoMapListingPage();
+ });
+
+ it('allows to manually type tag filter query', async () => {
+ await listingTable.searchForItemWithName('tag:(tag-1)', { escape: false });
+
+ await listingTable.expectItemsCount('map', 2);
+ const itemNames = await listingTable.getAllItemsNames();
+ expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 4 (tag-1)']);
+ });
+
+ it('allows to filter by selecting a tag in the filter menu', async () => {
+ await selectFilterTags('tag-3');
+
+ await listingTable.expectItemsCount('map', 2);
+ const itemNames = await listingTable.getAllItemsNames();
+ expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 2 (tag-3)']);
+ });
+
+ it('allows to filter by multiple tags', async () => {
+ await selectFilterTags('tag-2', 'tag-3');
+
+ await listingTable.expectItemsCount('map', 3);
+ const itemNames = await listingTable.getAllItemsNames();
+ expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 1 (tag-2)', 'map 2 (tag-3)']);
+ });
+ });
+
+ describe('creating', () => {
+ beforeEach(async () => {
+ await PageObjects.maps.openNewMap();
+ });
+
+ it('allows to select tags for a new map', async () => {
+ await PageObjects.maps.saveMap('my-new-map', false, ['tag-1', 'tag-3']);
+
+ await PageObjects.maps.gotoMapListingPage();
+ await selectFilterTags('tag-1');
+ const itemNames = await listingTable.getAllItemsNames();
+ expect(itemNames).to.contain('my-new-map');
+ });
+
+ it('allows to create a tag from the tag selector', async () => {
+ const { tagModal } = PageObjects.tagManagement;
+
+ await testSubjects.click('mapSaveButton');
+ await testSubjects.setValue('savedObjectTitle', 'map-with-new-tag');
+
+ await testSubjects.click('savedObjectTagSelector');
+ await testSubjects.click(`tagSelectorOption-action__create`);
+
+ expect(await tagModal.isOpened()).to.be(true);
+
+ await tagModal.fillForm(
+ {
+ name: 'my-new-tag',
+ color: '#FFCC33',
+ description: '',
+ },
+ {
+ submit: true,
+ }
+ );
+
+ expect(await tagModal.isOpened()).to.be(false);
+
+ await testSubjects.click('confirmSaveSavedObjectButton');
+
+ await PageObjects.maps.gotoMapListingPage();
+ await selectFilterTags('my-new-tag');
+ const itemNames = await listingTable.getAllItemsNames();
+ expect(itemNames).to.contain('map-with-new-tag');
+ });
+ });
+
+ describe('editing', () => {
+ beforeEach(async () => {
+ await PageObjects.common.navigateToUrlWithBrowserHistory('maps', '/');
+ });
+
+ it('allows to select tags for an existing map', async () => {
+ await listingTable.clickItemLink('map', 'map 4 (tag-1)');
+
+ await PageObjects.maps.saveMap('map 4 (tag-1)', false, ['tag-3']);
+
+ await PageObjects.maps.gotoMapListingPage();
+ await selectFilterTags('tag-3');
+ const itemNames = await listingTable.getAllItemsNames();
+ expect(itemNames).to.contain('map 4 (tag-1)');
+ });
+ });
+ });
+}
diff --git a/yarn.lock b/yarn.lock
index 3497fdf83d7dd..91ae4b236adf3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5277,10 +5277,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46"
integrity sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w==
-"@types/nodemailer@^6.2.1":
- version "6.2.1"
- resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.2.1.tgz#8f089bf0ef826f04b9d8dd8750233b04978cb675"
- integrity sha512-6f46rxxaFwyOW39psPoQiM7jHjL7apDRNT5WPHIuv+TZFv+7sBGSI9J7blIC3/NWff4O9/VSzgoQtO6aPLUdvQ==
+"@types/nodemailer@^6.4.0":
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.0.tgz#d8c039be3ed685c4719a026455555be82c124b74"
+ integrity sha512-KY7bFWB0MahRZvVW4CuW83qcCDny59pJJ0MQ5ifvfcjNwPlIT0vW4uARO4u1gtkYnWdhSvURegecY/tzcukJcA==
dependencies:
"@types/node" "*"
@@ -20791,10 +20791,10 @@ node-status-codes@^1.0.0:
resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f"
integrity sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=
-nodemailer@^4.7.0:
- version "4.7.0"
- resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.7.0.tgz#4420e06abfffd77d0618f184ea49047db84f4ad8"
- integrity sha512-IludxDypFpYw4xpzKdMAozBSkzKHmNBvGanUREjJItgJ2NYcK/s8+PggVhj7c2yGFQykKsnnmv1+Aqo0ZfjHmw==
+nodemailer@^6.4.16:
+ version "6.4.16"
+ resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.16.tgz#5cb6391b1d79ab7eff32d6f9f48366b5a7117293"
+ integrity sha512-68K0LgZ6hmZ7PVmwL78gzNdjpj5viqBdFqKrTtr9bZbJYj6BRj5W6WGkxXrEnUl3Co3CBXi3CZBUlpV/foGnOQ==
nodemon@^2.0.4:
version "2.0.6"