;
diff --git a/src/plugins/kibana_utils/common/persistable_state/types.ts b/src/plugins/kibana_utils/common/persistable_state/types.ts
index 6fea0a3a4eab6..80b791a75758f 100644
--- a/src/plugins/kibana_utils/common/persistable_state/types.ts
+++ b/src/plugins/kibana_utils/common/persistable_state/types.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import type { SerializableRecord } from '@kbn/utility-types';
+import type { SerializableRecord, Serializable } from '@kbn/utility-types';
import { SavedObjectReference } from '../../../../core/types';
/**
@@ -26,7 +26,7 @@ import { SavedObjectReference } from '../../../../core/types';
* };
* ```
*/
-export interface VersionedState {
+export interface VersionedState {
version: string;
state: S;
}
@@ -116,7 +116,7 @@ export type PersistableStateDefinition {
+export interface PersistableStateService
{
/**
* Function which reports telemetry information. This function is essentially
* a "reducer" - it receives the existing "stats" object and returns an
diff --git a/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge.js
index ca5021a882932..0e4322a7ba82c 100644
--- a/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge.js
+++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge.js
@@ -75,6 +75,7 @@ export class Gauge extends Component {
top: this.state.top || 0,
left: this.state.left || 0,
transform: `matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`,
+ zIndex: 1,
},
valueColor: {
color: this.props.valueColor,
diff --git a/src/plugins/vis_types/xy/public/config/get_config.test.ts b/src/plugins/vis_types/xy/public/config/get_config.test.ts
new file mode 100644
index 0000000000000..d046ac17c2b27
--- /dev/null
+++ b/src/plugins/vis_types/xy/public/config/get_config.test.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { getConfig } from './get_config';
+import { visData, visParamsWithTwoYAxes } from '../mocks';
+
+// ToDo: add more tests for all the config properties
+describe('getConfig', () => {
+ it('identifies it as a timeChart if the x axis has a date field', () => {
+ const config = getConfig(visData, visParamsWithTwoYAxes);
+ expect(config.isTimeChart).toBe(true);
+ });
+
+ it('not adds the current time marker if the param is set to false', () => {
+ const config = getConfig(visData, visParamsWithTwoYAxes);
+ expect(config.showCurrentTime).toBe(false);
+ });
+
+ it('adds the current time marker if the param is set to false', () => {
+ const newVisParams = {
+ ...visParamsWithTwoYAxes,
+ addTimeMarker: true,
+ };
+ const config = getConfig(visData, newVisParams);
+ expect(config.showCurrentTime).toBe(true);
+ });
+
+ it('enables the histogram mode for a date_histogram', () => {
+ const config = getConfig(visData, visParamsWithTwoYAxes);
+ expect(config.enableHistogramMode).toBe(true);
+ });
+
+ it('assigns the correct formatter per y axis', () => {
+ const config = getConfig(visData, visParamsWithTwoYAxes);
+ expect(config.yAxes.length).toBe(2);
+ expect(config.yAxes[0].ticks?.formatter).toStrictEqual(config.aspects.y[1].formatter);
+ expect(config.yAxes[1].ticks?.formatter).toStrictEqual(config.aspects.y[0].formatter);
+ });
+});
diff --git a/src/plugins/vis_types/xy/public/config/get_config.ts b/src/plugins/vis_types/xy/public/config/get_config.ts
index 13c9a6c275f8e..d2a3b9ad2a103 100644
--- a/src/plugins/vis_types/xy/public/config/get_config.ts
+++ b/src/plugins/vis_types/xy/public/config/get_config.ts
@@ -50,10 +50,16 @@ export function getConfig(table: Datatable, params: VisParams): VisConfig {
params.dimensions.x?.aggType === BUCKET_TYPES.DATE_HISTOGRAM
);
const tooltip = getTooltip(aspects, params);
- const yAxes = params.valueAxes.map((a) =>
- // uses first y aspect in array for formatting axis
- getAxis(a, params.grid, aspects.y[0], params.seriesParams)
- );
+ const yAxes = params.valueAxes.map((a) => {
+ // find the correct aspect for each value axis
+ const aspectsIdx = params.seriesParams.findIndex((s) => s.valueAxis === a.id);
+ return getAxis(
+ a,
+ params.grid,
+ aspects.y[aspectsIdx > -1 ? aspectsIdx : 0],
+ params.seriesParams
+ );
+ });
const enableHistogramMode =
(params.dimensions.x?.aggType === BUCKET_TYPES.DATE_HISTOGRAM ||
params.dimensions.x?.aggType === BUCKET_TYPES.HISTOGRAM) &&
diff --git a/src/plugins/vis_types/xy/public/mocks.ts b/src/plugins/vis_types/xy/public/mocks.ts
new file mode 100644
index 0000000000000..bb74035485723
--- /dev/null
+++ b/src/plugins/vis_types/xy/public/mocks.ts
@@ -0,0 +1,223 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import type { Datatable } from '../../../expressions/public';
+import type { VisParams } from './types';
+
+export const visData = {
+ type: 'datatable',
+ columns: [
+ {
+ id: 'col-0-2',
+ name: 'timestamp per 12 hours',
+ meta: {
+ type: 'date',
+ field: 'timestamp',
+ index: 'kibana_sample_data_logs',
+ },
+ },
+ {
+ id: 'col-1-3',
+ name: 'Average memory',
+ meta: {
+ type: 'number',
+ field: 'memory',
+ index: 'kibana_sample_data_logs',
+ },
+ },
+ {
+ id: 'col-2-1',
+ name: 'Average bytes',
+ meta: {
+ type: 'number',
+ field: 'bytes',
+ index: 'kibana_sample_data_logs',
+ },
+ },
+ ],
+ rows: [
+ {
+ 'col-0-2': 1632603600000,
+ 'col-1-3': 27400,
+ 'col-2-1': 6079.305555555556,
+ },
+ {
+ 'col-0-2': 1632646800000,
+ 'col-1-3': 148270,
+ 'col-2-1': 6164.056818181818,
+ },
+ {
+ 'col-0-2': 1632690000000,
+ 'col-1-3': 235280,
+ 'col-2-1': 6125.469387755102,
+ },
+ {
+ 'col-0-2': 1632733200000,
+ 'col-1-3': 206750,
+ 'col-2-1': 5362.68306010929,
+ },
+ ],
+} as Datatable;
+
+export const visParamsWithTwoYAxes = {
+ type: 'histogram',
+ addLegend: true,
+ addTooltip: true,
+ legendPosition: 'right',
+ addTimeMarker: false,
+ maxLegendLines: 1,
+ truncateLegend: true,
+ categoryAxes: [
+ {
+ type: 'category',
+ id: 'CategoryAxis-1',
+ show: true,
+ position: 'bottom',
+ title: {},
+ scale: {
+ type: 'linear',
+ },
+ labels: {
+ filter: true,
+ show: true,
+ truncate: 100,
+ },
+ },
+ ],
+ labels: {
+ type: 'label',
+ show: false,
+ },
+ thresholdLine: {
+ type: 'threshold_line',
+ show: false,
+ value: 10,
+ width: 1,
+ style: 'full',
+ color: '#E7664C',
+ },
+ valueAxes: [
+ {
+ type: 'value',
+ name: 'LeftAxis-1',
+ id: 'ValueAxis-1',
+ show: true,
+ position: 'left',
+ axisType: 'value',
+ title: {
+ text: 'my custom title',
+ },
+ scale: {
+ type: 'linear',
+ mode: 'normal',
+ scaleType: 'linear',
+ },
+ labels: {
+ type: 'label',
+ filter: false,
+ rotate: 0,
+ show: true,
+ truncate: 100,
+ },
+ },
+ {
+ type: 'value',
+ name: 'RightAxis-1',
+ id: 'ValueAxis-2',
+ show: true,
+ position: 'right',
+ title: {
+ text: 'my custom title',
+ },
+ scale: {
+ type: 'linear',
+ mode: 'normal',
+ },
+ labels: {
+ filter: false,
+ rotate: 0,
+ show: true,
+ truncate: 100,
+ },
+ },
+ ],
+ grid: {
+ categoryLines: false,
+ },
+ seriesParams: [
+ {
+ type: 'histogram',
+ data: {
+ label: 'Average memory',
+ id: '3',
+ },
+ drawLinesBetweenPoints: true,
+ interpolate: 'linear',
+ lineWidth: 2,
+ mode: 'stacked',
+ show: true,
+ showCircles: true,
+ circlesRadius: 3,
+ seriesParamType: 'histogram',
+ valueAxis: 'ValueAxis-2',
+ },
+ {
+ type: 'line',
+ data: {
+ label: 'Average bytes',
+ id: '1',
+ },
+ drawLinesBetweenPoints: true,
+ interpolate: 'linear',
+ lineWidth: 2,
+ mode: 'stacked',
+ show: true,
+ showCircles: true,
+ circlesRadius: 3,
+ valueAxis: 'ValueAxis-1',
+ },
+ ],
+ dimensions: {
+ x: {
+ type: 'xy_dimension',
+ label: 'timestamp per 12 hours',
+ aggType: 'date_histogram',
+ accessor: 0,
+ format: {
+ id: 'date',
+ params: {
+ pattern: 'YYYY-MM-DD HH:mm',
+ },
+ },
+ params: {
+ date: true,
+ },
+ },
+ y: [
+ {
+ label: 'Average memory',
+ aggType: 'avg',
+ params: {},
+ accessor: 1,
+ format: {
+ id: 'number',
+ params: {},
+ },
+ },
+ {
+ label: 'Average bytes',
+ aggType: 'avg',
+ params: {},
+ accessor: 2,
+ format: {
+ id: 'bytes',
+ params: {},
+ },
+ },
+ ],
+ },
+} as unknown as VisParams;
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
index 5868489934dc5..928cbec9c3747 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
@@ -30,7 +30,7 @@ import {
} from '../../../../plugins/embeddable/public';
import {
IExpressionLoaderParams,
- ExpressionsStart,
+ ExpressionLoader,
ExpressionRenderError,
ExpressionAstExpression,
} from '../../../../plugins/expressions/public';
@@ -81,8 +81,6 @@ export type VisualizeSavedObjectAttributes = SavedObjectAttributes & {
export type VisualizeByValueInput = { attributes: VisualizeSavedObjectAttributes } & VisualizeInput;
export type VisualizeByReferenceInput = SavedObjectEmbeddableInput & VisualizeInput;
-type ExpressionLoader = InstanceType;
-
export class VisualizeEmbeddable
extends Embeddable
implements ReferenceOrValueEmbeddable
@@ -302,7 +300,7 @@ export class VisualizeEmbeddable
super.render(this.domNode);
const expressions = getExpressions();
- this.handler = new expressions.ExpressionLoader(this.domNode, undefined, {
+ this.handler = await expressions.loader(this.domNode, undefined, {
onRenderError: (element: HTMLElement, error: ExpressionRenderError) => {
this.onContainerError(error);
},
diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts
index 832d895fcea3d..b7d19807e563e 100644
--- a/test/functional/apps/discover/_saved_queries.ts
+++ b/test/functional/apps/discover/_saved_queries.ts
@@ -28,9 +28,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const setUpQueriesWithFilters = async () => {
// set up a query with filters and a time filter
log.debug('set up a query with filters to save');
- const fromTime = 'Sep 20, 2015 @ 08:00:00.000';
- const toTime = 'Sep 21, 2015 @ 08:00:00.000';
- await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
+ const from = 'Sep 20, 2015 @ 08:00:00.000';
+ const to = 'Sep 21, 2015 @ 08:00:00.000';
+ await PageObjects.common.setTime({ from, to });
+ await PageObjects.common.navigateToApp('discover');
await filterBar.addFilter('extension.raw', 'is one of', 'jpg');
await queryBar.setQuery('response:200');
};
@@ -54,6 +55,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested');
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
+ await PageObjects.common.unsetTime();
});
describe('saved query selection', () => {
diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts
index 6cc4fda513ea6..bf2db6c25ad12 100644
--- a/test/functional/page_objects/time_picker.ts
+++ b/test/functional/page_objects/time_picker.ts
@@ -134,7 +134,7 @@ export class TimePickerPageObject extends FtrService {
});
// set from time
- await this.retry.waitFor(`endDate is set to ${fromTime}`, async () => {
+ await this.retry.waitFor(`startDate is set to ${fromTime}`, async () => {
await this.testSubjects.click('superDatePickerstartDatePopoverButton');
await this.waitPanelIsGone(panel);
panel = await this.getTimePickerPanel();
@@ -150,6 +150,7 @@ export class TimePickerPageObject extends FtrService {
});
await this.retry.waitFor('Timepicker popover to close', async () => {
+ await this.browser.pressKeys(this.browser.keys.ESCAPE);
return !(await this.testSubjects.exists('superDatePickerAbsoluteDateInput'));
});
diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts
index 7d629ee817252..82a9f9b7d3f27 100644
--- a/test/functional/services/remote/webdriver.ts
+++ b/test/functional/services/remote/webdriver.ts
@@ -71,6 +71,60 @@ export interface BrowserConfig {
acceptInsecureCerts: boolean;
}
+function initChromiumOptions(browserType: Browsers, acceptInsecureCerts: boolean) {
+ const options = browserType === Browsers.Chrome ? new chrome.Options() : new edge.Options();
+
+ options.addArguments(
+ // Disables the sandbox for all process types that are normally sandboxed.
+ 'no-sandbox',
+ // Launches URL in new browser window.
+ 'new-window',
+ // By default, file:// URIs cannot read other file:// URIs. This is an override for developers who need the old behavior for testing.
+ 'allow-file-access-from-files',
+ // Use fake device for Media Stream to replace actual camera and microphone.
+ 'use-fake-device-for-media-stream',
+ // Bypass the media stream infobar by selecting the default device for media streams (e.g. WebRTC). Works with --use-fake-device-for-media-stream.
+ 'use-fake-ui-for-media-stream'
+ );
+
+ if (process.platform === 'linux') {
+ // The /dev/shm partition is too small in certain VM environments, causing
+ // Chrome to fail or crash. Use this flag to work-around this issue
+ // (a temporary directory will always be used to create anonymous shared memory files).
+ options.addArguments('disable-dev-shm-usage');
+ }
+
+ if (headlessBrowser === '1') {
+ // Use --disable-gpu to avoid an error from a missing Mesa library, as per
+ // See: https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
+ options.headless();
+ options.addArguments('disable-gpu');
+ }
+
+ if (certValidation === '0') {
+ options.addArguments('ignore-certificate-errors');
+ }
+
+ if (remoteDebug === '1') {
+ // Visit chrome://inspect in chrome to remotely view/debug
+ options.headless();
+ options.addArguments('disable-gpu', 'remote-debugging-port=9222');
+ }
+
+ if (browserBinaryPath) {
+ options.setChromeBinaryPath(browserBinaryPath);
+ }
+
+ const prefs = new logging.Preferences();
+ prefs.setLevel(logging.Type.BROWSER, logging.Level.ALL);
+ options.setUserPreferences(chromiumUserPrefs);
+ options.setLoggingPrefs(prefs);
+ options.set('unexpectedAlertBehaviour', 'accept');
+ options.setAcceptInsecureCerts(acceptInsecureCerts);
+
+ return options;
+}
+
let attemptCounter = 0;
let edgePaths: { driverPath: string | undefined; browserPath: string | undefined };
async function attemptToCreateCommand(
@@ -86,55 +140,10 @@ async function attemptToCreateCommand(
const buildDriverInstance = async () => {
switch (browserType) {
case 'chrome': {
- const chromeOptions = new chrome.Options();
- chromeOptions.addArguments(
- // Disables the sandbox for all process types that are normally sandboxed.
- 'no-sandbox',
- // Launches URL in new browser window.
- 'new-window',
- // By default, file:// URIs cannot read other file:// URIs. This is an override for developers who need the old behavior for testing.
- 'allow-file-access-from-files',
- // Use fake device for Media Stream to replace actual camera and microphone.
- 'use-fake-device-for-media-stream',
- // Bypass the media stream infobar by selecting the default device for media streams (e.g. WebRTC). Works with --use-fake-device-for-media-stream.
- 'use-fake-ui-for-media-stream'
- );
-
- if (process.platform === 'linux') {
- // The /dev/shm partition is too small in certain VM environments, causing
- // Chrome to fail or crash. Use this flag to work-around this issue
- // (a temporary directory will always be used to create anonymous shared memory files).
- chromeOptions.addArguments('disable-dev-shm-usage');
- }
-
- if (headlessBrowser === '1') {
- // Use --disable-gpu to avoid an error from a missing Mesa library, as per
- // See: https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
- chromeOptions.headless();
- chromeOptions.addArguments('disable-gpu');
- }
-
- if (certValidation === '0') {
- chromeOptions.addArguments('ignore-certificate-errors');
- }
-
- if (remoteDebug === '1') {
- // Visit chrome://inspect in chrome to remotely view/debug
- chromeOptions.headless();
- chromeOptions.addArguments('disable-gpu', 'remote-debugging-port=9222');
- }
-
- if (browserBinaryPath) {
- chromeOptions.setChromeBinaryPath(browserBinaryPath);
- }
-
- const prefs = new logging.Preferences();
- prefs.setLevel(logging.Type.BROWSER, logging.Level.ALL);
- chromeOptions.setUserPreferences(chromiumUserPrefs);
- chromeOptions.setLoggingPrefs(prefs);
- chromeOptions.set('unexpectedAlertBehaviour', 'accept');
- chromeOptions.setAcceptInsecureCerts(config.acceptInsecureCerts);
-
+ const chromeOptions = initChromiumOptions(
+ browserType,
+ config.acceptInsecureCerts
+ ) as chrome.Options;
let session;
if (remoteSessionUrl) {
session = await new Builder()
@@ -169,19 +178,10 @@ async function attemptToCreateCommand(
case 'msedge': {
if (edgePaths && edgePaths.browserPath && edgePaths.driverPath) {
- const edgeOptions = new edge.Options();
- if (headlessBrowser === '1') {
- // @ts-ignore internal modules are not typed
- edgeOptions.headless();
- }
- // @ts-ignore internal modules are not typed
- edgeOptions.setEdgeChromium(true);
- // @ts-ignore internal modules are not typed
- edgeOptions.setBinaryPath(edgePaths.browserPath);
- const options = edgeOptions.get('ms:edgeOptions');
- // overriding options to include preferences
- Object.assign(options, { prefs: chromiumUserPrefs });
- edgeOptions.set('ms:edgeOptions', options);
+ const edgeOptions = initChromiumOptions(
+ browserType,
+ config.acceptInsecureCerts
+ ) as edge.Options;
const session = await new Builder()
.forBrowser('MicrosoftEdge')
.setEdgeOptions(edgeOptions)
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json
index 67316b1d0d7eb..e78f294cde7c9 100644
--- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json
+++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json
@@ -9,5 +9,5 @@
"requiredPlugins": ["data", "savedObjects", "kibanaUtils", "expressions"],
"server": true,
"ui": true,
- "requiredBundles": ["inspector"]
+ "requiredBundles": []
}
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx
index b03451bdebad2..6d132c3acb730 100644
--- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx
+++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx
@@ -13,10 +13,8 @@ import { first, pluck } from 'rxjs/operators';
import {
IInterpreterRenderHandlers,
ExpressionValue,
- TablesAdapter,
} from '../../../../../../../src/plugins/expressions/public';
-import { RequestAdapter } from '../../../../../../../src/plugins/inspector/public';
-import { Adapters, ExpressionRenderHandler } from '../../types';
+import { ExpressionRenderHandler } from '../../types';
import { getExpressions } from '../../services';
declare global {
@@ -50,13 +48,9 @@ class Main extends React.Component<{}, State> {
initialContext: ExpressionValue = {}
) => {
this.setState({ expression });
- const adapters: Adapters = {
- requests: new RequestAdapter(),
- tables: new TablesAdapter(),
- };
+
return getExpressions()
.execute(expression, context || { type: 'null' }, {
- inspectorAdapters: adapters,
searchContext: initialContext as any,
})
.getData()
@@ -70,7 +64,7 @@ class Main extends React.Component<{}, State> {
lastRenderHandler.destroy();
}
- lastRenderHandler = getExpressions().render(this.chartRef.current!, context, {
+ lastRenderHandler = await getExpressions().render(this.chartRef.current!, context, {
onRenderError: (el: HTMLElement, error: unknown, handler: IInterpreterRenderHandlers) => {
this.setState({
expression: 'Render error!\n\n' + JSON.stringify(error),
diff --git a/x-pack/plugins/apm/dev_docs/linting.md b/x-pack/plugins/apm/dev_docs/linting.md
index 7db7053e59061..edf3e813a88e9 100644
--- a/x-pack/plugins/apm/dev_docs/linting.md
+++ b/x-pack/plugins/apm/dev_docs/linting.md
@@ -17,5 +17,5 @@ yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write
### ESLint
```
-node scripts/eslint.js x-pack/legacy/plugins/apm
+node scripts/eslint.js x-pack/plugins/apm
```
diff --git a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx
index ee6a58b0dbb76..fb1a99db0bf5b 100644
--- a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx
+++ b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx
@@ -103,7 +103,7 @@ export function ChartPreview({
data={data}
id="chart_preview_bar_series"
xAccessor="x"
- xScaleType={ScaleType.Linear}
+ xScaleType={ScaleType.Time}
yAccessors={['y']}
yScaleType={ScaleType.Linear}
/>
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts
index 615ba84afb5b7..db04712271587 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts
@@ -153,7 +153,7 @@ export function getKeywordSettings(item: FieldVisConfig) {
accessors: ['col2'],
layerId: 'layer1',
layerType: 'data',
- seriesType: 'bar',
+ seriesType: 'bar_horizontal',
xAccessor: 'col1',
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx
index 754d0e470fe40..b56e6ae815645 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx
@@ -7,7 +7,7 @@
import React, { FC, ReactNode, useMemo } from 'react';
import { EuiBasicTable, EuiSpacer, RIGHT_ALIGNMENT, HorizontalAlignment } from '@elastic/eui';
-import { Axis, BarSeries, Chart, Settings } from '@elastic/charts';
+import { Axis, BarSeries, Chart, Settings, ScaleType } from '@elastic/charts';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -137,9 +137,9 @@ export const BooleanContent: FC = ({ config }) => {
splitSeriesAccessors={['x']}
stackAccessors={['x']}
xAccessor="x"
- xScaleType="ordinal"
+ xScaleType={ScaleType.Ordinal}
yAccessors={['count']}
- yScaleType="linear"
+ yScaleType={ScaleType.Linear}
/>
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/boolean_content_preview.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/boolean_content_preview.tsx
index ceb2e6f241682..a44cd2a2e83c9 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/boolean_content_preview.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/boolean_content_preview.tsx
@@ -37,6 +37,7 @@ export const BooleanContentPreview: FC = ({ config }) => {
chartData={chartData}
columnType={columnType}
hideLabel={true}
+ maxChartColumns={10}
/>
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.tsx
index 453754d4d6bd4..8b0ba08498600 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.tsx
@@ -22,7 +22,7 @@ interface Props {
columnType: EuiDataGridColumn;
dataTestSubj: string;
hideLabel?: boolean;
- maxChartColumns?: number;
+ maxChartColumns: number;
}
const zeroSize = { bottom: 0, left: 0, right: 0, top: 0 };
@@ -42,8 +42,12 @@ export const ColumnChart: FC = ({
{!isUnsupportedChartData(chartData) && data.length > 0 && (
i)}
+ theme={{
+ chartMargins: zeroSize,
+ chartPaddings: zeroSize,
+ crosshair: { band: { visible: false } },
+ }}
/>
= ({
/>
{
describe('getLegendText()', () => {
it('should return the chart legend text for unsupported chart types', () => {
- expect(getLegendText(validUnsupportedChartData)).toBe('Chart not supported.');
+ expect(getLegendText(validUnsupportedChartData, 20)).toBe('Chart not supported.');
});
it('should return the chart legend text for empty datasets', () => {
- expect(getLegendText(validNumericChartData)).toBe('0 documents contain field.');
+ expect(getLegendText(validNumericChartData, 20)).toBe('0 documents contain field.');
});
it('should return the chart legend text for boolean chart types', () => {
const { getByText } = render(
<>
- {getLegendText({
- cardinality: 2,
- data: [
- { key: 'true', key_as_string: 'true', doc_count: 10 },
- { key: 'false', key_as_string: 'false', doc_count: 20 },
- ],
- id: 'the-id',
- type: 'boolean',
- })}
+ {getLegendText(
+ {
+ cardinality: 2,
+ data: [
+ { key: 'true', key_as_string: 'true', doc_count: 10 },
+ { key: 'false', key_as_string: 'false', doc_count: 20 },
+ ],
+ id: 'the-id',
+ type: 'boolean',
+ },
+ 20
+ )}
>
);
expect(getByText('t')).toBeInTheDocument();
expect(getByText('f')).toBeInTheDocument();
});
it('should return the chart legend text for ordinal chart data with less than max categories', () => {
- expect(getLegendText({ ...validOrdinalChartData, data: [{ key: 'cat', doc_count: 10 }] })).toBe(
- '10 categories'
- );
+ expect(
+ getLegendText({ ...validOrdinalChartData, data: [{ key: 'cat', doc_count: 10 }] }, 20)
+ ).toBe('10 categories');
});
it('should return the chart legend text for ordinal chart data with more than max categories', () => {
expect(
- getLegendText({
- ...validOrdinalChartData,
- cardinality: 30,
- data: [{ key: 'cat', doc_count: 10 }],
- })
+ getLegendText(
+ {
+ ...validOrdinalChartData,
+ cardinality: 30,
+ data: [{ key: 'cat', doc_count: 10 }],
+ },
+ 20
+ )
).toBe('top 20 of 30 categories');
});
it('should return the chart legend text for numeric datasets', () => {
expect(
- getLegendText({
- ...validNumericChartData,
- data: [{ key: 1, doc_count: 10 }],
- stats: [1, 100],
- })
+ getLegendText(
+ {
+ ...validNumericChartData,
+ data: [{ key: 1, doc_count: 10 }],
+ stats: [1, 100],
+ },
+ 20
+ )
).toBe('1 - 100');
expect(
- getLegendText({
- ...validNumericChartData,
- data: [{ key: 1, doc_count: 10 }],
- stats: [100, 100],
- })
+ getLegendText(
+ {
+ ...validNumericChartData,
+ data: [{ key: 1, doc_count: 10 }],
+ stats: [100, 100],
+ },
+ 20
+ )
).toBe('100');
expect(
- getLegendText({
- ...validNumericChartData,
- data: [{ key: 1, doc_count: 10 }],
- stats: [1.2345, 6.3456],
- })
+ getLegendText(
+ {
+ ...validNumericChartData,
+ data: [{ key: 1, doc_count: 10 }],
+ stats: [1.2345, 6.3456],
+ },
+ 20
+ )
).toBe('1.23 - 6.35');
});
});
@@ -167,7 +182,7 @@ describe('getLegendText()', () => {
describe('useColumnChart()', () => {
it('should return the column chart hook data', () => {
const { result } = renderHook(() =>
- useColumnChart(validNumericChartData, { id: 'the-id', schema: 'numeric' })
+ useColumnChart(validNumericChartData, { id: 'the-id', schema: 'numeric' }, 20)
);
expect(result.current.data).toStrictEqual([]);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx
index 2c0817228655e..60e1595c64ece 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx
@@ -32,7 +32,6 @@ export const hoveredRow$ = new BehaviorSubject(null);
export const BAR_COLOR = euiPaletteColorBlind()[0];
const BAR_COLOR_BLUR = euiPaletteColorBlind({ rotations: 2 })[10];
-const MAX_CHART_COLUMNS = 20;
type XScaleType = 'ordinal' | 'time' | 'linear' | undefined;
export const getXScaleType = (kbnFieldType: KBN_FIELD_TYPES | undefined): XScaleType => {
@@ -76,10 +75,7 @@ export const getFieldType = (schema: EuiDataGridColumn['schema']): KBN_FIELD_TYP
};
type LegendText = string | JSX.Element;
-export const getLegendText = (
- chartData: ChartData,
- maxChartColumns = MAX_CHART_COLUMNS
-): LegendText => {
+export const getLegendText = (chartData: ChartData, maxChartColumns: number): LegendText => {
if (chartData.type === 'unsupported') {
return i18n.translate('xpack.dataVisualizer.dataGridChart.histogramNotAvailable', {
defaultMessage: 'Chart not supported.',
@@ -146,7 +142,7 @@ interface ColumnChart {
export const useColumnChart = (
chartData: ChartData,
columnType: EuiDataGridColumn,
- maxChartColumns?: number
+ maxChartColumns: number
): ColumnChart => {
const fieldType = getFieldType(columnType.schema);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx
index f7e9f5437fc3f..42d808da6d9ee 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx
@@ -25,6 +25,7 @@ import { CurationsSettings } from './curations_settings';
describe('Curations', () => {
const values = {
+ // CurationsLogic
dataLoading: false,
curations: [
{
@@ -46,6 +47,8 @@ describe('Curations', () => {
},
},
selectedPageTab: 'overview',
+ // LicensingLogic
+ hasPlatinumLicense: true,
};
const actions = {
@@ -75,6 +78,20 @@ describe('Curations', () => {
tabs.at(2).simulate('click');
expect(actions.onSelectPageTab).toHaveBeenNthCalledWith(3, 'settings');
+ // The settings tab should NOT have an icon next to it
+ expect(tabs.at(2).prop('prepend')).toBeUndefined();
+ });
+
+ it('renders less tabs when less than platinum license', () => {
+ setMockValues({ ...values, hasPlatinumLicense: false });
+ const wrapper = shallow();
+
+ expect(getPageTitle(wrapper)).toEqual('Curated results');
+
+ const tabs = getPageHeaderTabs(wrapper).find(EuiTab);
+ expect(tabs.length).toBe(2);
+ // The settings tab should have an icon next to it
+ expect(tabs.at(1).prop('prepend')).not.toBeUndefined();
});
it('renders an overview view', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx
index c55fde7626488..7440e0cf42b44 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx
@@ -9,8 +9,10 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
+import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { LicensingLogic } from '../../../../shared/licensing';
import { EuiButtonTo } from '../../../../shared/react_router_helpers';
import { ENGINE_CURATIONS_NEW_PATH } from '../../../routes';
@@ -28,39 +30,47 @@ import { CurationsSettings } from './curations_settings';
export const Curations: React.FC = () => {
const { dataLoading, curations, meta, selectedPageTab } = useValues(CurationsLogic);
const { loadCurations, onSelectPageTab } = useActions(CurationsLogic);
+ const { hasPlatinumLicense } = useValues(LicensingLogic);
- const pageTabs = [
- {
- label: i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.curations.overviewPageTabLabel',
- {
- defaultMessage: 'Overview',
- }
- ),
- isSelected: selectedPageTab === 'overview',
- onClick: () => onSelectPageTab('overview'),
- },
- {
- label: i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.curations.historyPageTabLabel',
- {
- defaultMessage: 'History',
- }
- ),
- isSelected: selectedPageTab === 'history',
- onClick: () => onSelectPageTab('history'),
- },
- {
- label: i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.curations.settingsPageTabLabel',
+ const OVERVIEW_TAB = {
+ label: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curations.overviewPageTabLabel',
+ {
+ defaultMessage: 'Overview',
+ }
+ ),
+ isSelected: selectedPageTab === 'overview',
+ onClick: () => onSelectPageTab('overview'),
+ };
+
+ const HISTORY_TAB = {
+ label: i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.historyPageTabLabel', {
+ defaultMessage: 'History',
+ }),
+ isSelected: selectedPageTab === 'history',
+ onClick: () => onSelectPageTab('history'),
+ };
+
+ const SETTINGS_TAB = {
+ label: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curations.settingsPageTabLabel',
+ {
+ defaultMessage: 'Settings',
+ }
+ ),
+ isSelected: selectedPageTab === 'settings',
+ onClick: () => onSelectPageTab('settings'),
+ };
+
+ const pageTabs = hasPlatinumLicense
+ ? [OVERVIEW_TAB, HISTORY_TAB, SETTINGS_TAB]
+ : [
+ OVERVIEW_TAB,
{
- defaultMessage: 'Settings',
- }
- ),
- isSelected: selectedPageTab === 'settings',
- onClick: () => onSelectPageTab('settings'),
- },
- ];
+ ...SETTINGS_TAB,
+ prepend: ,
+ },
+ ];
useEffect(() => {
loadCurations();
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
index a43fb6f293457..a0df5337b2e2e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
@@ -288,6 +288,9 @@ export const SOURCE_OBJ_TYPES = {
CAMPAIGNS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.campaigns', {
defaultMessage: 'Campaigns',
}),
+ CASES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.cases', {
+ defaultMessage: 'Cases (including feeds and comments)',
+ }),
USERS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.users', {
defaultMessage: 'Users',
}),
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx
index c303190651f57..244ce79135cab 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx
@@ -432,6 +432,7 @@ export const staticSourceData = [
SOURCE_OBJ_TYPES.LEADS,
SOURCE_OBJ_TYPES.ACCOUNTS,
SOURCE_OBJ_TYPES.CAMPAIGNS,
+ SOURCE_OBJ_TYPES.CASES,
],
features: {
basicOrgContext: [
@@ -466,6 +467,7 @@ export const staticSourceData = [
SOURCE_OBJ_TYPES.LEADS,
SOURCE_OBJ_TYPES.ACCOUNTS,
SOURCE_OBJ_TYPES.CAMPAIGNS,
+ SOURCE_OBJ_TYPES.CASES,
],
features: {
basicOrgContext: [
diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx
index 0ad6378a22960..c5f5ace2f5470 100644
--- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx
+++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx
@@ -224,7 +224,7 @@ const CriterionPreviewChart: React.FC = ({
-
+
diff --git a/x-pack/plugins/lens/common/expressions/merge_tables/merge_tables.test.ts b/x-pack/plugins/lens/common/expressions/merge_tables/merge_tables.test.ts
index c883f6b7cb479..cd4fc5ed945d4 100644
--- a/x-pack/plugins/lens/common/expressions/merge_tables/merge_tables.test.ts
+++ b/x-pack/plugins/lens/common/expressions/merge_tables/merge_tables.test.ts
@@ -58,6 +58,7 @@ describe('lens_merge_tables', () => {
const adapters: DefaultInspectorAdapters = {
tables: new TablesAdapter(),
requests: {} as never,
+ expression: {} as never,
};
mergeTables.fn(null, { layerIds: ['first', 'second'], tables: [sampleTable1, sampleTable2] }, {
inspectorAdapters: adapters,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index 9c22ec9d4bb05..854075a94dcaa 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -27,7 +27,7 @@ import {
} from '@elastic/eui';
import {
Axis,
- BarSeries,
+ HistogramBarSeries,
Chart,
niceTimeFormatter,
Position,
@@ -636,7 +636,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
showOverlappingTicks={true}
/>
- formatter.convert(d)}
/>
- {
try {
const { id, list_id: listId, namespace_type: namespaceType } = request.query;
- const exceptionLists = getExceptionListClient(context);
- const exceptionList = await exceptionLists.getExceptionList({
+ const exceptionListsClient = getExceptionListClient(context);
+
+ const exportContent = await exceptionListsClient.exportExceptionListAndItems({
id,
listId,
namespaceType,
});
- if (exceptionList == null) {
+ if (exportContent == null) {
return siemResponse.error({
- body: `exception list with list_id: ${listId} does not exist`,
+ body: `exception list with list_id: ${listId} or id: ${id} does not exist`,
statusCode: 400,
});
- } else {
- const listItems = await exceptionLists.findExceptionListItem({
- filter: undefined,
- listId,
- namespaceType,
- page: 1,
- perPage: 10000,
- sortField: 'exception-list.created_at',
- sortOrder: 'desc',
- });
- const exceptionItems = listItems?.data ?? [];
-
- const { exportData } = getExport([exceptionList, ...exceptionItems]);
- const { exportDetails } = getExportDetails(exceptionItems);
-
- // TODO: Allow the API to override the name of the file to export
- const fileName = exceptionList.list_id;
- return response.ok({
- body: `${exportData}${exportDetails}`,
- headers: {
- 'Content-Disposition': `attachment; filename="${fileName}"`,
- 'Content-Type': 'application/ndjson',
- },
- });
}
+
+ return response.ok({
+ body: `${exportContent.exportData}${JSON.stringify(exportContent.exportDetails)}\n`,
+ headers: {
+ 'Content-Disposition': `attachment; filename="${listId}"`,
+ 'Content-Type': 'application/ndjson',
+ },
+ });
} catch (err) {
const error = transformError(err);
return siemResponse.error({
@@ -77,24 +61,3 @@ export const exportExceptionListRoute = (router: ListsPluginRouter): void => {
}
);
};
-
-export const getExport = (
- data: unknown[]
-): {
- exportData: string;
-} => {
- const ndjson = transformDataToNdjson(data);
-
- return { exportData: ndjson };
-};
-
-export const getExportDetails = (
- items: unknown[]
-): {
- exportDetails: string;
-} => {
- const exportDetails = JSON.stringify({
- exported_list_items_count: items.length,
- });
- return { exportDetails: `${exportDetails}\n` };
-};
diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts
index 1566241e7351e..f5f6a4f1f2d5a 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts
@@ -30,6 +30,17 @@ export class ExceptionListClientMock extends ExceptionListClient {
public findExceptionList = jest.fn().mockResolvedValue(getFoundExceptionListSchemaMock());
public createTrustedAppsList = jest.fn().mockResolvedValue(getTrustedAppsListSchemaMock());
public createEndpointList = jest.fn().mockResolvedValue(getExceptionListSchemaMock());
+ public exportExceptionListAndItems = jest.fn().mockResolvedValue({
+ exportData: 'exportString',
+ exportDetails: {
+ exported_exception_list_count: 0,
+ exported_exception_list_item_count: 0,
+ missing_exception_list_item_count: 0,
+ missing_exception_list_items: [],
+ missing_exception_lists: [],
+ missing_exception_lists_count: 0,
+ },
+ });
}
export const getExceptionListClientMock = (
diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
index 77e82bf0f7578..542598fc82c90 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
@@ -24,6 +24,7 @@ import {
DeleteExceptionListItemByIdOptions,
DeleteExceptionListItemOptions,
DeleteExceptionListOptions,
+ ExportExceptionListAndItemsOptions,
FindEndpointListItemOptions,
FindExceptionListItemOptions,
FindExceptionListOptions,
@@ -38,6 +39,10 @@ import {
UpdateExceptionListOptions,
} from './exception_list_client_types';
import { getExceptionList } from './get_exception_list';
+import {
+ ExportExceptionListAndItemsReturn,
+ exportExceptionListAndItems,
+} from './export_exception_list_and_items';
import { getExceptionListSummary } from './get_exception_list_summary';
import { createExceptionList } from './create_exception_list';
import { getExceptionListItem } from './get_exception_list_item';
@@ -492,4 +497,19 @@ export class ExceptionListClient {
sortOrder,
});
};
+
+ public exportExceptionListAndItems = async ({
+ listId,
+ id,
+ namespaceType,
+ }: ExportExceptionListAndItemsOptions): Promise => {
+ const { savedObjectsClient } = this;
+
+ return exportExceptionListAndItems({
+ id,
+ listId,
+ namespaceType,
+ savedObjectsClient,
+ });
+ };
}
diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts
index b734d3a7b1a3b..14de474974c11 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts
@@ -220,3 +220,21 @@ export interface FindExceptionListOptions {
sortField: SortFieldOrUndefined;
sortOrder: SortOrderOrUndefined;
}
+
+export interface ExportExceptionListAndItemsOptions {
+ listId: ListIdOrUndefined;
+ id: IdOrUndefined;
+ namespaceType: NamespaceType;
+}
+
+export interface ExportExceptionListAndItemsReturn {
+ exportData: string;
+ exportDetails: {
+ exported_exception_list_count: number;
+ exported_exception_list_item_count: number;
+ missing_exception_list_item_count: number;
+ missing_exception_list_items: string[];
+ missing_exception_lists: string[];
+ missing_exception_lists_count: number;
+ };
+}
diff --git a/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.test.ts b/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.test.ts
new file mode 100644
index 0000000000000..9f3c02fecca20
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.test.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SavedObjectsClientContract } from 'kibana/server';
+
+import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock';
+import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock';
+
+import { exportExceptionListAndItems } from './export_exception_list_and_items';
+import { findExceptionListItem } from './find_exception_list_item';
+import { getExceptionList } from './get_exception_list';
+
+jest.mock('./get_exception_list');
+jest.mock('./find_exception_list_item');
+
+describe('export_exception_list_and_items', () => {
+ describe('exportExceptionListAndItems', () => {
+ test('it should return null if no matching exception list found', async () => {
+ (getExceptionList as jest.Mock).mockResolvedValue(null);
+ (findExceptionListItem as jest.Mock).mockResolvedValue({ data: [] });
+
+ const result = await exportExceptionListAndItems({
+ id: '123',
+ listId: 'non-existent',
+ namespaceType: 'single',
+ savedObjectsClient: {} as SavedObjectsClientContract,
+ });
+ expect(result).toBeNull();
+ });
+
+ test('it should return stringified list and items', async () => {
+ (getExceptionList as jest.Mock).mockResolvedValue(getExceptionListSchemaMock());
+ (findExceptionListItem as jest.Mock).mockResolvedValue({
+ data: [getExceptionListItemSchemaMock()],
+ });
+
+ const result = await exportExceptionListAndItems({
+ id: '123',
+ listId: 'non-existent',
+ namespaceType: 'single',
+ savedObjectsClient: {} as SavedObjectsClientContract,
+ });
+ expect(result?.exportData).toEqual(
+ `${JSON.stringify(getExceptionListSchemaMock())}\n${JSON.stringify(
+ getExceptionListItemSchemaMock()
+ )}\n`
+ );
+ expect(result?.exportDetails).toEqual({
+ exported_exception_list_count: 1,
+ exported_exception_list_item_count: 1,
+ missing_exception_list_item_count: 0,
+ missing_exception_list_items: [],
+ missing_exception_lists: [],
+ missing_exception_lists_count: 0,
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.ts b/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.ts
new file mode 100644
index 0000000000000..46b3df4e5ac44
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type {
+ IdOrUndefined,
+ ListIdOrUndefined,
+ NamespaceType,
+} from '@kbn/securitysolution-io-ts-list-types';
+import { transformDataToNdjson } from '@kbn/securitysolution-utils';
+import { SavedObjectsClientContract } from 'kibana/server';
+
+import { findExceptionListItem } from './find_exception_list_item';
+import { getExceptionList } from './get_exception_list';
+
+interface ExportExceptionListAndItemsOptions {
+ id: IdOrUndefined;
+ listId: ListIdOrUndefined;
+ savedObjectsClient: SavedObjectsClientContract;
+ namespaceType: NamespaceType;
+}
+
+export interface ExportExceptionListAndItemsReturn {
+ exportData: string;
+ exportDetails: {
+ exported_exception_list_count: number;
+ exported_exception_list_item_count: number;
+ missing_exception_list_item_count: number;
+ missing_exception_list_items: string[];
+ missing_exception_lists: string[];
+ missing_exception_lists_count: number;
+ };
+}
+
+export const exportExceptionListAndItems = async ({
+ id,
+ listId,
+ namespaceType,
+ savedObjectsClient,
+}: ExportExceptionListAndItemsOptions): Promise => {
+ const exceptionList = await getExceptionList({
+ id,
+ listId,
+ namespaceType,
+ savedObjectsClient,
+ });
+
+ if (exceptionList == null) {
+ return null;
+ } else {
+ // TODO: Will need to address this when we switch over to
+ // using PIT, don't want it to get lost
+ // https://github.com/elastic/kibana/issues/103944
+ const listItems = await findExceptionListItem({
+ filter: undefined,
+ listId: exceptionList.list_id,
+ namespaceType: exceptionList.namespace_type,
+ page: 1,
+ perPage: 10000,
+ savedObjectsClient,
+ sortField: 'exception-list.created_at',
+ sortOrder: 'desc',
+ });
+ const exceptionItems = listItems?.data ?? [];
+ const { exportData } = getExport([exceptionList, ...exceptionItems]);
+
+ // TODO: Add logic for missing lists and items on errors
+ return {
+ exportData: `${exportData}`,
+ exportDetails: {
+ exported_exception_list_count: 1,
+ exported_exception_list_item_count: exceptionItems.length,
+ missing_exception_list_item_count: 0,
+ missing_exception_list_items: [],
+ missing_exception_lists: [],
+ missing_exception_lists_count: 0,
+ },
+ };
+ }
+};
+
+export const getExport = (
+ data: unknown[]
+): {
+ exportData: string;
+} => {
+ const ndjson = transformDataToNdjson(data);
+
+ return { exportData: ndjson };
+};
diff --git a/x-pack/plugins/lists/server/services/exception_lists/index.ts b/x-pack/plugins/lists/server/services/exception_lists/index.ts
index e6a6dd7ef8c3c..fbc052936931a 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/index.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/index.ts
@@ -10,6 +10,7 @@ export * from './create_exception_list_item';
export * from './delete_exception_list';
export * from './delete_exception_list_item';
export * from './delete_exception_list_items_by_list';
+export * from './export_exception_list_and_items';
export * from './find_exception_list';
export * from './find_exception_list_item';
export * from './find_exception_list_items';
diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js
index a25a28c4da21c..162c305a69ca5 100644
--- a/x-pack/plugins/maps/server/routes.js
+++ b/x-pack/plugins/maps/server/routes.js
@@ -488,10 +488,11 @@ export async function initRoutes(core, getLicenseId, emsSettings, kbnVersion, lo
},
(context, request, response) => {
const range = path.normalize(request.params.range);
- return range.startsWith('..')
+ const rootPath = path.resolve(__dirname, 'fonts', 'open_sans');
+ const fontPath = path.resolve(rootPath, `${range}.pbf`);
+ return !fontPath.startsWith(rootPath)
? response.notFound()
: new Promise((resolve) => {
- const fontPath = path.join(__dirname, 'fonts', 'open_sans', `${range}.pbf`);
fs.readFile(fontPath, (error, data) => {
if (error) {
resolve(response.notFound());
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx
index 3800256927d54..9ed7714dd8059 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx
+++ b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx
@@ -8,7 +8,7 @@
import React, { FC } from 'react';
import classNames from 'classnames';
-import { BarSeries, Chart, Settings } from '@elastic/charts';
+import { BarSeries, Chart, Settings, ScaleType } from '@elastic/charts';
import { EuiDataGridColumn } from '@elastic/eui';
import './column_chart.scss';
@@ -48,7 +48,7 @@ export const ColumnChart: FC = ({
hideLabel,
maxChartColumns,
}) => {
- const { data, legendText, xScaleType } = useColumnChart(chartData, columnType, maxChartColumns);
+ const { data, legendText } = useColumnChart(chartData, columnType, maxChartColumns);
return (
@@ -59,8 +59,8 @@ export const ColumnChart: FC
= ({
d.datum.color}
diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx
index fedb07fa65a40..2543b054ee5bb 100644
--- a/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx
@@ -15,7 +15,7 @@ import { useTable } from '../../hooks/use_table';
import { ApmTemplate } from './apm_template';
// @ts-ignore
import { ApmServerInstances } from '../../../components/apm/instances';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
import { APM_SYSTEM_ID } from '../../../../common/constants';
diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx
index 4611f17159621..b33789f510f2e 100644
--- a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx
@@ -15,7 +15,7 @@ import { useTable } from '../../hooks/use_table';
import { BeatsTemplate } from './beats_template';
// @ts-ignore
import { Listing } from '../../../components/beats/listing';
-import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
import { BEATS_SYSTEM_ID } from '../../../../common/constants';
diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx
index 04074762c8d22..2ffbc3a75ce05 100644
--- a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx
@@ -14,7 +14,7 @@ import { GlobalStateContext } from '../../contexts/global_state_context';
import { TabMenuItem } from '../page_template';
import { Overview } from '../../../components/cluster/overview';
import { ExternalConfigContext } from '../../contexts/external_config_context';
-import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
import { fetchClusters } from '../../../lib/fetch_clusters';
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx
index cb37705c959aa..2ab18331d1cdb 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx
@@ -13,7 +13,7 @@ import { GlobalStateContext } from '../../contexts/global_state_context';
// @ts-ignore
import { Ccr } from '../../../components/elasticsearch/ccr';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { AlertsByName } from '../../../alerts/types';
import { fetchAlerts } from '../../../lib/fetch_alerts';
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx
index 29cf9ade8d997..2ded26df16323 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx
@@ -15,7 +15,7 @@ import { GlobalStateContext } from '../../contexts/global_state_context';
// @ts-ignore
import { CcrShardReact } from '../../../components/elasticsearch/ccr_shard';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { AlertsByName } from '../../../alerts/types';
import { fetchAlerts } from '../../../lib/fetch_alerts';
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx
index f2f2ec36b7cd9..c51027636b287 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx
@@ -11,7 +11,7 @@ import { useParams } from 'react-router-dom';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { GlobalStateContext } from '../../contexts/global_state_context';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useCharts } from '../../hooks/use_charts';
import { ItemTemplate } from './item_template';
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
index 8e70a99e67914..422f051c7d718 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
@@ -13,7 +13,7 @@ import { GlobalStateContext } from '../../contexts/global_state_context';
// @ts-ignore
import { IndexReact } from '../../../components/elasticsearch/index/index_react';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useCharts } from '../../hooks/use_charts';
import { ItemTemplate } from './item_template';
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx
index 277bde2ac35cb..6618db7eebe66 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx
@@ -12,7 +12,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
import { GlobalStateContext } from '../../contexts/global_state_context';
import { ElasticsearchIndices } from '../../../components/elasticsearch';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useTable } from '../../hooks/use_table';
import { useLocalStorage } from '../../hooks/use_local_storage';
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx
index b97007f1c1462..46bb4cc20242f 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx
@@ -12,7 +12,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
import { GlobalStateContext } from '../../contexts/global_state_context';
import { ElasticsearchMLJobs } from '../../../components/elasticsearch';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useTable } from '../../hooks/use_table';
import type { MLJobs } from '../../../types';
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
index b2d6fb94183ec..a75c8447a3561 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
@@ -13,7 +13,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
import { GlobalStateContext } from '../../contexts/global_state_context';
import { NodeReact } from '../../../components/elasticsearch';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useLocalStorage } from '../../hooks/use_local_storage';
import { useCharts } from '../../hooks/use_charts';
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx
index ac7e267cbc9ac..9933188b887d5 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx
@@ -13,7 +13,7 @@ import { GlobalStateContext } from '../../contexts/global_state_context';
import { ExternalConfigContext } from '../../contexts/external_config_context';
import { ElasticsearchNodes } from '../../../components/elasticsearch';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useTable } from '../../hooks/use_table';
import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx
index 076e9413216fb..a27c1418eabc1 100644
--- a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx
@@ -16,7 +16,7 @@ import { KibanaTemplate } from './kibana_template';
// @ts-ignore
import { KibanaInstances } from '../../../components/kibana/instances';
// @ts-ignore
-import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
import { AlertsByName } from '../../../alerts/types';
diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx
index 0fd10a93bcd83..447a7b1792fb9 100644
--- a/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx
@@ -13,7 +13,7 @@ import { ComponentProps } from '../../route_init';
// @ts-ignore
import { Listing } from '../../../components/logstash/listing';
import { LogstashTemplate } from './logstash_template';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer } from '../../../components/renderers/setup_mode';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useTable } from '../../hooks/use_table';
import { LOGSTASH_SYSTEM_ID, RULE_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants';
diff --git a/x-pack/plugins/monitoring/public/application/route_init.tsx b/x-pack/plugins/monitoring/public/application/route_init.tsx
index 52780aa280707..92def5b2c36f2 100644
--- a/x-pack/plugins/monitoring/public/application/route_init.tsx
+++ b/x-pack/plugins/monitoring/public/application/route_init.tsx
@@ -9,7 +9,7 @@ import { Route, Redirect, useLocation } from 'react-router-dom';
import { useClusters } from './hooks/use_clusters';
import { GlobalStateContext } from './contexts/global_state_context';
import { getClusterFromClusters } from '../lib/get_cluster_from_clusters';
-import { isInSetupMode } from './setup_mode';
+import { isInSetupMode } from '../lib/setup_mode';
import { LoadingPage } from './pages/loading_page';
export interface ComponentProps {
diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js
deleted file mode 100644
index df524fa99ae53..0000000000000
--- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { Fragment } from 'react';
-import {
- getSetupModeState,
- initSetupModeState,
- updateSetupModeData,
- disableElasticsearchInternalCollection,
- toggleSetupMode,
- setSetupModeMenuItem,
-} from '../../lib/setup_mode';
-import { Flyout } from '../../components/metricbeat_migration/flyout';
-import {
- EuiBottomBar,
- EuiButton,
- EuiFlexGroup,
- EuiFlexItem,
- EuiTextColor,
- EuiIcon,
- EuiSpacer,
-} from '@elastic/eui';
-import { findNewUuid } from '../../components/renderers/lib/find_new_uuid';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { GlobalStateContext } from '../../application/contexts/global_state_context';
-import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
-import { useRequestErrorHandler } from '../hooks/use_request_error_handler';
-
-class WrappedSetupModeRenderer extends React.Component {
- globalState;
- state = {
- renderState: false,
- isFlyoutOpen: false,
- instance: null,
- newProduct: null,
- isSettingUpNew: false,
- };
-
- UNSAFE_componentWillMount() {
- this.globalState = this.context;
- const { kibana, onHttpError } = this.props;
- initSetupModeState(this.globalState, kibana.services.http, onHttpError, (_oldData) => {
- const newState = { renderState: true };
-
- const { productName } = this.props;
- if (!productName) {
- this.setState(newState);
- return;
- }
-
- const setupModeState = getSetupModeState();
- if (!setupModeState.enabled || !setupModeState.data) {
- this.setState(newState);
- return;
- }
-
- const data = setupModeState.data[productName];
- const oldData = _oldData ? _oldData[productName] : null;
- if (data && oldData) {
- const newUuid = findNewUuid(Object.keys(oldData.byUuid), Object.keys(data.byUuid));
- if (newUuid) {
- newState.newProduct = data.byUuid[newUuid];
- }
- }
-
- this.setState(newState);
- });
- setSetupModeMenuItem();
- }
-
- reset() {
- this.setState({
- renderState: false,
- isFlyoutOpen: false,
- instance: null,
- newProduct: null,
- isSettingUpNew: false,
- });
- }
-
- getFlyout(data, meta) {
- const { productName } = this.props;
- const { isFlyoutOpen, instance, isSettingUpNew, newProduct } = this.state;
- if (!data || !isFlyoutOpen) {
- return null;
- }
-
- let product = null;
- if (newProduct) {
- product = newProduct;
- }
- // For new instance discovery flow, we pass in empty instance object
- else if (instance && Object.keys(instance).length) {
- product = data.byUuid[instance.uuid];
- }
-
- if (!product) {
- const uuids = Object.values(data.byUuid);
- if (uuids.length && !isSettingUpNew) {
- product = uuids[0];
- } else {
- product = {
- isNetNewUser: true,
- };
- }
- }
-
- return (
- this.reset()}
- productName={productName}
- product={product}
- meta={meta}
- instance={instance}
- updateProduct={updateSetupModeData}
- isSettingUpNew={isSettingUpNew}
- />
- );
- }
-
- getBottomBar(setupModeState) {
- if (!setupModeState.enabled || setupModeState.hideBottomBar) {
- return null;
- }
-
- return (
-
-
-
-
-
-
-
-
- ,
- }}
- />
-
-
-
-
-
-
-
- toggleSetupMode(false)}
- >
- {i18n.translate('xpack.monitoring.setupMode.exit', {
- defaultMessage: `Exit setup mode`,
- })}
-
-
-
-
-
-
-
- );
- }
-
- async shortcutToFinishMigration() {
- await disableElasticsearchInternalCollection();
- await updateSetupModeData();
- }
-
- render() {
- const { render, productName } = this.props;
- const setupModeState = getSetupModeState();
-
- let data = { byUuid: {} };
- if (setupModeState.data) {
- if (productName && setupModeState.data[productName]) {
- data = setupModeState.data[productName];
- } else if (setupModeState.data) {
- data = setupModeState.data;
- }
- }
-
- const meta = setupModeState.data ? setupModeState.data._meta : null;
-
- return render({
- setupMode: {
- data,
- meta,
- enabled: setupModeState.enabled,
- productName,
- updateSetupModeData,
- shortcutToFinishMigration: () => this.shortcutToFinishMigration(),
- openFlyout: (instance, isSettingUpNew) =>
- this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }),
- closeFlyout: () => this.setState({ isFlyoutOpen: false }),
- },
- flyoutComponent: this.getFlyout(data, meta),
- bottomBarComponent: this.getBottomBar(setupModeState),
- });
- }
-}
-
-function withErrorHandler(Component) {
- return function WrappedComponent(props) {
- const handleRequestError = useRequestErrorHandler();
- return ;
- };
-}
-
-WrappedSetupModeRenderer.contextType = GlobalStateContext;
-export const SetupModeRenderer = withKibana(withErrorHandler(WrappedSetupModeRenderer));
diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.d.ts
similarity index 100%
rename from x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts
rename to x-pack/plugins/monitoring/public/components/renderers/setup_mode.d.ts
diff --git a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js
index 573f7e7e33c5e..cfa57559d2bc9 100644
--- a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js
+++ b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js
@@ -27,8 +27,12 @@ import {
import { findNewUuid } from './lib/find_new_uuid';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
+import { GlobalStateContext } from '../../application/contexts/global_state_context';
+import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
+import { useRequestErrorHandler } from '../../application/hooks/use_request_error_handler';
-export class SetupModeRenderer extends React.Component {
+export class WrappedSetupModeRenderer extends React.Component {
+ globalState;
state = {
renderState: false,
isFlyoutOpen: false,
@@ -38,9 +42,11 @@ export class SetupModeRenderer extends React.Component {
};
UNSAFE_componentWillMount() {
- const { scope, injector } = this.props;
- initSetupModeState(scope, injector, (_oldData) => {
+ this.globalState = this.context;
+ const { kibana, onHttpError } = this.props;
+ initSetupModeState(this.globalState, kibana.services.http, onHttpError, (_oldData) => {
const newState = { renderState: true };
+
const { productName } = this.props;
if (!productName) {
this.setState(newState);
@@ -207,3 +213,13 @@ export class SetupModeRenderer extends React.Component {
});
}
}
+
+function withErrorHandler(Component) {
+ return function WrappedComponent(props) {
+ const handleRequestError = useRequestErrorHandler();
+ return ;
+ };
+}
+
+WrappedSetupModeRenderer.contextType = GlobalStateContext;
+export const SetupModeRenderer = withKibana(withErrorHandler(WrappedSetupModeRenderer));
diff --git a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js
index cb1593d084366..9672da8b8088a 100644
--- a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js
+++ b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js
@@ -9,6 +9,14 @@ import React, { Fragment } from 'react';
import { shallow } from 'enzyme';
import { ELASTICSEARCH_SYSTEM_ID } from '../../../common/constants';
+const kibanaMock = {
+ services: {
+ http: jest.fn(),
+ },
+};
+
+const onHttpErrorMock = jest.fn();
+
describe('SetupModeRenderer', () => {
beforeEach(() => jest.resetModules());
@@ -21,16 +29,14 @@ describe('SetupModeRenderer', () => {
updateSetupModeData: () => {},
setSetupModeMenuItem: () => {},
}));
- const SetupModeRenderer = require('./setup_mode').SetupModeRenderer;
+ const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
const ChildComponent = () => Hi
;
- const scope = {};
- const injector = {};
const component = shallow(
(
{flyoutComponent}
@@ -57,16 +63,14 @@ describe('SetupModeRenderer', () => {
updateSetupModeData: () => {},
setSetupModeMenuItem: () => {},
}));
- const SetupModeRenderer = require('./setup_mode').SetupModeRenderer;
+ const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
const ChildComponent = () => Hi
;
- const scope = {};
- const injector = {};
const component = shallow(
(
{flyoutComponent}
@@ -95,16 +99,14 @@ describe('SetupModeRenderer', () => {
updateSetupModeData: () => {},
setSetupModeMenuItem: () => {},
}));
- const SetupModeRenderer = require('./setup_mode').SetupModeRenderer;
+ const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
const ChildComponent = () => Hi
;
- const scope = {};
- const injector = {};
const component = shallow(
(
{flyoutComponent}
@@ -135,16 +137,14 @@ describe('SetupModeRenderer', () => {
updateSetupModeData: () => {},
setSetupModeMenuItem: () => {},
}));
- const SetupModeRenderer = require('./setup_mode').SetupModeRenderer;
+ const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
const ChildComponent = () => Hi
;
- const scope = {};
- const injector = {};
const component = shallow(
(
{flyoutComponent}
@@ -176,7 +176,7 @@ describe('SetupModeRenderer', () => {
_meta: {},
},
}),
- initSetupModeState: (_scope, _injectir, cb) => {
+ initSetupModeState: (_globalState, _httpService, _onError, cb) => {
setTimeout(() => {
cb({
elasticsearch: {
@@ -190,16 +190,14 @@ describe('SetupModeRenderer', () => {
updateSetupModeData: () => {},
setSetupModeMenuItem: () => {},
}));
- const SetupModeRenderer = require('./setup_mode').SetupModeRenderer;
+ const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
const ChildComponent = () => Hi
;
- const scope = {};
- const injector = {};
const component = shallow(
(
{flyoutComponent}
@@ -235,7 +233,7 @@ describe('SetupModeRenderer', () => {
_meta: {},
},
}),
- initSetupModeState: (_scope, _injectir, cb) => {
+ initSetupModeState: (_globalState, _httpService, _onError, cb) => {
setTimeout(() => {
cb({
elasticsearch: {
@@ -249,16 +247,14 @@ describe('SetupModeRenderer', () => {
updateSetupModeData: () => {},
setSetupModeMenuItem,
}));
- const SetupModeRenderer = require('./setup_mode').SetupModeRenderer;
+ const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer;
const ChildComponent = () => Hi
;
- const scope = {};
- const injector = {};
const component = shallow(
(
{flyoutComponent}
diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/plugins/monitoring/public/lib/setup_mode.test.js
index 47cae9c4f0851..bacc305764d68 100644
--- a/x-pack/plugins/monitoring/public/lib/setup_mode.test.js
+++ b/x-pack/plugins/monitoring/public/lib/setup_mode.test.js
@@ -11,11 +11,8 @@ let getSetupModeState;
let updateSetupModeData;
let setSetupModeMenuItem;
-jest.mock('./ajax_error_handler', () => ({
- ajaxErrorHandlersProvider: (err) => {
- throw err;
- },
-}));
+const handleErrorsMock = jest.fn();
+const callbackMock = jest.fn();
jest.mock('react-dom', () => ({
render: jest.fn(),
@@ -25,7 +22,6 @@ jest.mock('../legacy_shims', () => {
return {
Legacy: {
shims: {
- getAngularInjector: () => ({ get: () => ({ get: () => 'utc' }) }),
toastNotifications: {
addDanger: jest.fn(),
},
@@ -35,41 +31,8 @@ jest.mock('../legacy_shims', () => {
};
});
-let data = {};
-
-const injectorModulesMock = {
- globalState: {
- save: jest.fn(),
- },
- Private: (module) => module,
- $http: {
- post: jest.fn().mockImplementation(() => {
- return { data };
- }),
- },
- $executor: {
- run: jest.fn(),
- },
-};
-
-const angularStateMock = {
- injector: {
- get: (module) => {
- return injectorModulesMock[module] || {};
- },
- },
- scope: {
- $apply: (fn) => fn && fn(),
- $evalAsync: (fn) => fn && fn(),
- },
-};
-
-// We are no longer waiting for setup mode data to be fetched when enabling
-// so we need to wait for the next tick for the async action to finish
-
function setModulesAndMocks() {
jest.clearAllMocks().resetModules();
- injectorModulesMock.globalState.inSetupMode = false;
const setupMode = require('./setup_mode');
toggleSetupMode = setupMode.toggleSetupMode;
@@ -83,53 +46,76 @@ function waitForSetupModeData() {
return new Promise((resolve) => process.nextTick(resolve));
}
-xdescribe('setup_mode', () => {
+describe('setup_mode', () => {
beforeEach(async () => {
setModulesAndMocks();
});
describe('setup', () => {
- it('should require angular state', async () => {
- let error;
- try {
- toggleSetupMode(true);
- } catch (err) {
- error = err;
- }
- expect(error.message).toEqual(
- 'Unable to interact with setup ' +
- 'mode because the angular injector was not previously set. This needs to be ' +
- 'set by calling `initSetupModeState`.'
- );
- });
-
it('should enable toggle mode', async () => {
- await initSetupModeState(angularStateMock.scope, angularStateMock.injector);
+ const globalState = {
+ inSetupMode: false,
+ save: jest.fn(),
+ };
+ const httpServiceMock = {
+ post: jest.fn(),
+ };
+
+ await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock);
toggleSetupMode(true);
- expect(injectorModulesMock.globalState.inSetupMode).toBe(true);
+ expect(globalState.inSetupMode).toBe(true);
});
it('should disable toggle mode', async () => {
- await initSetupModeState(angularStateMock.scope, angularStateMock.injector);
+ const globalState = {
+ inSetupMode: true,
+ save: jest.fn(),
+ };
+ const httpServiceMock = {
+ post: jest.fn(),
+ };
+ const handleErrorsMock = jest.fn();
+ const callbackMock = jest.fn();
+ await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock);
toggleSetupMode(false);
- expect(injectorModulesMock.globalState.inSetupMode).toBe(false);
+ expect(globalState.inSetupMode).toBe(false);
});
it('should set top nav config', async () => {
+ const globalState = {
+ inSetupMode: false,
+ save: jest.fn(),
+ };
+ const httpServiceMock = {
+ post: jest.fn(),
+ };
+
const render = require('react-dom').render;
- await initSetupModeState(angularStateMock.scope, angularStateMock.injector);
+
+ await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock);
setSetupModeMenuItem();
toggleSetupMode(true);
+
expect(render.mock.calls.length).toBe(2);
});
});
describe('in setup mode', () => {
- afterEach(async () => {
- data = {};
- });
-
it('should not fetch data if the user does not have sufficient permissions', async () => {
+ const globalState = {
+ inSetupMode: false,
+ save: jest.fn(),
+ };
+ const httpServiceMock = {
+ post: jest.fn().mockReturnValue(
+ Promise.resolve({
+ _meta: {
+ hasPermissions: false,
+ },
+ })
+ ),
+ };
+
const addDanger = jest.fn();
jest.doMock('../legacy_shims', () => ({
Legacy: {
@@ -141,13 +127,9 @@ xdescribe('setup_mode', () => {
},
},
}));
- data = {
- _meta: {
- hasPermissions: false,
- },
- };
+
setModulesAndMocks();
- await initSetupModeState(angularStateMock.scope, angularStateMock.injector);
+ await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock);
toggleSetupMode(true);
await waitForSetupModeData();
@@ -160,78 +142,122 @@ xdescribe('setup_mode', () => {
});
it('should set the newly discovered cluster uuid', async () => {
+ const globalState = {
+ inSetupMode: false,
+ cluster_uuid: undefined,
+ save: jest.fn(),
+ };
const clusterUuid = '1ajy';
- data = {
- _meta: {
- liveClusterUuid: clusterUuid,
- hasPermissions: true,
- },
- elasticsearch: {
- byUuid: {
- 123: {
- isPartiallyMigrated: true,
+ const httpServiceMock = {
+ post: jest.fn().mockReturnValue(
+ Promise.resolve({
+ _meta: {
+ liveClusterUuid: clusterUuid,
+ hasPermissions: true,
},
- },
- },
+ elasticsearch: {
+ byUuid: {
+ 123: {
+ isPartiallyMigrated: true,
+ },
+ },
+ },
+ })
+ ),
};
- await initSetupModeState(angularStateMock.scope, angularStateMock.injector);
+
+ await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock);
toggleSetupMode(true);
await waitForSetupModeData();
- expect(injectorModulesMock.globalState.cluster_uuid).toBe(clusterUuid);
+ expect(globalState.cluster_uuid).toBe(clusterUuid);
});
it('should fetch data for a given cluster', async () => {
const clusterUuid = '1ajy';
- data = {
- _meta: {
- liveClusterUuid: clusterUuid,
- hasPermissions: true,
- },
- elasticsearch: {
- byUuid: {
- 123: {
- isPartiallyMigrated: true,
+ const globalState = {
+ inSetupMode: false,
+ cluster_uuid: clusterUuid,
+ save: jest.fn(),
+ };
+ const httpServiceMock = {
+ post: jest.fn().mockReturnValue(
+ Promise.resolve({
+ _meta: {
+ liveClusterUuid: clusterUuid,
+ hasPermissions: true,
},
- },
- },
+ elasticsearch: {
+ byUuid: {
+ 123: {
+ isPartiallyMigrated: true,
+ },
+ },
+ },
+ })
+ ),
};
- await initSetupModeState(angularStateMock.scope, angularStateMock.injector);
+ await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock);
toggleSetupMode(true);
await waitForSetupModeData();
- expect(injectorModulesMock.$http.post).toHaveBeenCalledWith(
+ expect(httpServiceMock.post).toHaveBeenCalledWith(
`../api/monitoring/v1/setup/collection/cluster/${clusterUuid}`,
- {
- ccs: undefined,
- }
+ { body: '{}' }
);
});
it('should fetch data for a single node', async () => {
- await initSetupModeState(angularStateMock.scope, angularStateMock.injector);
+ const clusterUuid = '1ajy';
+ const globalState = {
+ inSetupMode: false,
+ save: jest.fn(),
+ };
+ const httpServiceMock = {
+ post: jest.fn().mockReturnValue(
+ Promise.resolve({
+ _meta: {
+ liveClusterUuid: clusterUuid,
+ hasPermissions: true,
+ },
+ elasticsearch: {
+ byUuid: {
+ 123: {
+ isPartiallyMigrated: true,
+ },
+ },
+ },
+ })
+ ),
+ };
+
+ await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock);
toggleSetupMode(true);
await waitForSetupModeData();
- injectorModulesMock.$http.post.mockClear();
await updateSetupModeData('45asd');
- expect(injectorModulesMock.$http.post).toHaveBeenCalledWith(
+ expect(httpServiceMock.post).toHaveBeenCalledWith(
'../api/monitoring/v1/setup/collection/node/45asd',
- {
- ccs: undefined,
- }
+ { body: '{}' }
);
});
it('should fetch data without a cluster uuid', async () => {
- initSetupModeState(angularStateMock.scope, angularStateMock.injector);
+ const globalState = {
+ inSetupMode: false,
+ save: jest.fn(),
+ };
+ const httpServiceMock = {
+ post: jest.fn(),
+ };
+
+ await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock);
await toggleSetupMode(true);
- injectorModulesMock.$http.post.mockClear();
await updateSetupModeData(undefined, true);
const url = '../api/monitoring/v1/setup/collection/cluster';
- const args = { ccs: undefined };
- expect(injectorModulesMock.$http.post).toHaveBeenCalledWith(url, args);
+ const args = { body: '{}' };
+ expect(httpServiceMock.post).toHaveBeenCalledWith(url, args);
});
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
index f3f332b5094b6..ceb87429f9de4 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
@@ -7,8 +7,8 @@
import { i18n } from '@kbn/i18n';
import React, { useEffect, useRef, useState } from 'react';
-import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui';
import styled from 'styled-components';
+import { EuiButtonEmpty, EuiResizableContainer, EuiTitle, EuiPanel } from '@elastic/eui';
import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
@@ -20,6 +20,7 @@ import { useAppIndexPatternContext } from './hooks/use_app_index_pattern';
import { SeriesViews } from './views/series_views';
import { LensEmbeddable } from './lens_embeddable';
import { EmptyView } from './components/empty_view';
+import type { ChartTimeRange } from './header/last_updated';
export type PanelId = 'seriesPanel' | 'chartPanel';
@@ -37,7 +38,7 @@ export function ExploratoryView({
const [height, setHeight] = useState('100vh');
- const [lastUpdated, setLastUpdated] = useState();
+ const [chartTimeRangeContext, setChartTimeRangeContext] = useState();
const [lensAttributes, setLensAttributes] = useState(
null
@@ -96,7 +97,10 @@ export function ExploratoryView({
{lens ? (
<>
-
+
{lensAttributes ? (
) : (
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.test.tsx
new file mode 100644
index 0000000000000..570362a63c33f
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.test.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { screen } from '@testing-library/dom';
+import { render } from '../rtl_helpers';
+import { ChartCreationInfo } from './chart_creation_info';
+
+const info = {
+ to: 1634071132571,
+ from: 1633406400000,
+ lastUpdated: 1634071140788,
+};
+
+describe('ChartCreationInfo', () => {
+ it('renders chart creation info', async () => {
+ render();
+
+ expect(screen.getByText('Chart created')).toBeInTheDocument();
+ expect(screen.getByText('Oct 12, 2021 4:39 PM')).toBeInTheDocument();
+ expect(screen.getByText('Displaying from')).toBeInTheDocument();
+ expect(screen.getByText('Oct 5, 2021 12:00 AM → Oct 12, 2021 4:38 PM')).toBeInTheDocument();
+ });
+
+ it('does not display info when props are falsey', async () => {
+ render();
+
+ expect(screen.queryByText('Chart created')).not.toBeInTheDocument();
+ expect(screen.queryByText('Displaying from')).not.toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.tsx
new file mode 100644
index 0000000000000..4814bc8d8630a
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import moment from 'moment';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui';
+import type { ChartTimeRange } from './last_updated';
+
+export function ChartCreationInfo(props: Partial) {
+ const dateFormat = 'lll';
+ const from = moment(props.from).format(dateFormat);
+ const to = moment(props.to).format(dateFormat);
+ const created = moment(props.lastUpdated).format(dateFormat);
+
+ return (
+ <>
+ {props.lastUpdated && (
+ <>
+
+
+
+
+
+
+
+ {created}
+
+
+
+ >
+ )}
+ {props.to && props.from && (
+ <>
+
+
+
+
+
+
+
+
+ {from} → {to}
+
+
+
+ >
+ )}
+ >
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
index 181c8342b87af..22245f111293c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
@@ -10,16 +10,17 @@ import { i18n } from '@kbn/i18n';
import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { TypedLensByValueInput } from '../../../../../../lens/public';
import { useSeriesStorage } from '../hooks/use_series_storage';
-import { LastUpdated } from './last_updated';
import { ExpViewActionMenu } from '../components/action_menu';
import { useExpViewTimeRange } from '../hooks/use_time_range';
+import { LastUpdated } from './last_updated';
+import type { ChartTimeRange } from './last_updated';
interface Props {
- lastUpdated?: number;
+ chartTimeRange?: ChartTimeRange;
lensAttributes: TypedLensByValueInput['attributes'] | null;
}
-export function ExploratoryViewHeader({ lensAttributes, lastUpdated }: Props) {
+export function ExploratoryViewHeader({ lensAttributes, chartTimeRange }: Props) {
const { setLastRefresh } = useSeriesStorage();
const timeRange = useExpViewTimeRange();
@@ -46,7 +47,7 @@ export function ExploratoryViewHeader({ lensAttributes, lastUpdated }: Props) {
-
+
setLastRefresh(Date.now())}>
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx
index c352ec0423dd8..bc82c48214a01 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx
@@ -6,14 +6,24 @@
*/
import React, { useEffect, useState } from 'react';
-import { EuiIcon, EuiText } from '@elastic/eui';
import moment from 'moment';
+import styled from 'styled-components';
+import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { ChartCreationInfo } from './chart_creation_info';
+
+export interface ChartTimeRange {
+ lastUpdated: number;
+ to: number;
+ from: number;
+}
interface Props {
- lastUpdated?: number;
+ chartTimeRange?: ChartTimeRange;
}
-export function LastUpdated({ lastUpdated }: Props) {
+
+export function LastUpdated({ chartTimeRange }: Props) {
+ const { lastUpdated } = chartTimeRange || {};
const [refresh, setRefresh] = useState(() => Date.now());
useEffect(() => {
@@ -39,7 +49,13 @@ export function LastUpdated({ lastUpdated }: Props) {
return (
-
+ }
+ >
+
+ {' '}
);
}
+
+export const StyledToolTipWrapper = styled.div`
+ min-width: 30vw;
+`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx
index 235790e72862c..b3ec7ee184f00 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx
@@ -13,14 +13,16 @@ import { useSeriesStorage } from './hooks/use_series_storage';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { useExpViewTimeRange } from './hooks/use_time_range';
+import { parseRelativeDate } from './components/date_range_picker';
+import type { ChartTimeRange } from './header/last_updated';
interface Props {
lensAttributes: TypedLensByValueInput['attributes'];
- setLastUpdated: Dispatch>;
+ setChartTimeRangeContext: Dispatch>;
}
export function LensEmbeddable(props: Props) {
- const { lensAttributes, setLastUpdated } = props;
+ const { lensAttributes, setChartTimeRangeContext } = props;
const {
services: { lens, notifications },
@@ -35,8 +37,12 @@ export function LensEmbeddable(props: Props) {
const timeRange = useExpViewTimeRange();
const onLensLoad = useCallback(() => {
- setLastUpdated(Date.now());
- }, [setLastUpdated]);
+ setChartTimeRangeContext({
+ lastUpdated: Date.now(),
+ to: parseRelativeDate(timeRange?.to || '').valueOf(),
+ from: parseRelativeDate(timeRange?.from || '').valueOf(),
+ });
+ }, [setChartTimeRangeContext, timeRange]);
const onBrushEnd = useCallback(
({ range }: { range: number[] }) => {
diff --git a/x-pack/plugins/security/common/index.ts b/x-pack/plugins/security/common/index.ts
index ac5d252c98a8b..1d05036191635 100644
--- a/x-pack/plugins/security/common/index.ts
+++ b/x-pack/plugins/security/common/index.ts
@@ -6,4 +6,4 @@
*/
export type { SecurityLicense } from './licensing';
-export type { AuthenticatedUser } from './model';
+export type { AuthenticatedUser, PrivilegeDeprecationsService } from './model';
diff --git a/x-pack/plugins/security/server/deprecations/index.ts b/x-pack/plugins/security/server/deprecations/index.ts
index 05802a5a673c5..2c4b47ba41a0a 100644
--- a/x-pack/plugins/security/server/deprecations/index.ts
+++ b/x-pack/plugins/security/server/deprecations/index.ts
@@ -5,8 +5,9 @@
* 2.0.
*/
-/**
- * getKibanaRolesByFeature
- */
-
export { getPrivilegeDeprecationsService } from './privilege_deprecations';
+export {
+ registerKibanaUserRoleDeprecation,
+ KIBANA_ADMIN_ROLE_NAME,
+ KIBANA_USER_ROLE_NAME,
+} from './kibana_user_role';
diff --git a/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts b/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts
new file mode 100644
index 0000000000000..da728b12fca91
--- /dev/null
+++ b/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts
@@ -0,0 +1,328 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { errors } from '@elastic/elasticsearch';
+import type { SecurityRoleMapping, SecurityUser } from '@elastic/elasticsearch/api/types';
+
+import type { PackageInfo, RegisterDeprecationsConfig } from 'src/core/server';
+import {
+ deprecationsServiceMock,
+ elasticsearchServiceMock,
+ loggingSystemMock,
+ savedObjectsClientMock,
+} from 'src/core/server/mocks';
+
+import { licenseMock } from '../../common/licensing/index.mock';
+import { securityMock } from '../mocks';
+import { registerKibanaUserRoleDeprecation } from './kibana_user_role';
+
+function getDepsMock() {
+ return {
+ logger: loggingSystemMock.createLogger(),
+ deprecationsService: deprecationsServiceMock.createSetupContract(),
+ license: licenseMock.create(),
+ packageInfo: {
+ branch: 'some-branch',
+ buildSha: 'sha',
+ dist: true,
+ version: '8.0.0',
+ buildNum: 1,
+ } as PackageInfo,
+ };
+}
+
+function getContextMock() {
+ return {
+ esClient: elasticsearchServiceMock.createScopedClusterClient(),
+ savedObjectsClient: savedObjectsClientMock.create(),
+ };
+}
+
+function createMockUser(user: Partial = {}) {
+ return { enabled: true, username: 'userA', roles: ['roleA'], metadata: {}, ...user };
+}
+
+function createMockRoleMapping(mapping: Partial = {}) {
+ return { enabled: true, roles: ['roleA'], rules: {}, metadata: {}, ...mapping };
+}
+
+describe('Kibana Dashboard Only User role deprecations', () => {
+ let mockDeps: ReturnType;
+ let mockContext: ReturnType;
+ let deprecationHandler: RegisterDeprecationsConfig;
+ beforeEach(() => {
+ mockContext = getContextMock();
+ mockDeps = getDepsMock();
+ registerKibanaUserRoleDeprecation(mockDeps);
+
+ expect(mockDeps.deprecationsService.registerDeprecations).toHaveBeenCalledTimes(1);
+ deprecationHandler = mockDeps.deprecationsService.registerDeprecations.mock.calls[0][0];
+ });
+
+ it('does not return any deprecations if security is not enabled', async () => {
+ mockDeps.license.isEnabled.mockReturnValue(false);
+
+ await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toEqual([]);
+ expect(mockContext.esClient.asCurrentUser.security.getUser).not.toHaveBeenCalled();
+ expect(mockContext.esClient.asCurrentUser.security.getRoleMapping).not.toHaveBeenCalled();
+ });
+
+ it('does not return any deprecations if none of the users and role mappings has a kibana user role', async () => {
+ mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue(
+ securityMock.createApiResponse({ body: { userA: createMockUser() } })
+ );
+
+ mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ mappingA: { enabled: true, roles: ['roleA'], rules: {}, metadata: {} },
+ },
+ })
+ );
+
+ await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toEqual([]);
+ });
+
+ it('returns deprecations even if cannot retrieve users due to permission error', async () => {
+ mockContext.esClient.asCurrentUser.security.getUser.mockRejectedValue(
+ new errors.ResponseError(securityMock.createApiResponse({ statusCode: 403, body: {} }))
+ );
+ mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue(
+ securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } })
+ );
+
+ await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "correctiveActions": Object {
+ "manualSteps": Array [
+ "Make sure you have a \\"manage_security\\" cluster privilege assigned.",
+ ],
+ },
+ "deprecationType": "feature",
+ "documentationUrl": "https://www.elastic.co/guide/en/kibana/some-branch/xpack-security.html#_required_permissions_7",
+ "level": "fetch_error",
+ "message": "You do not have enough permissions to fix this deprecation.",
+ "title": "The \\"kibana_user\\" role is deprecated",
+ },
+ ]
+ `);
+ });
+
+ it('returns deprecations even if cannot retrieve users due to unknown error', async () => {
+ mockContext.esClient.asCurrentUser.security.getUser.mockRejectedValue(
+ new errors.ResponseError(securityMock.createApiResponse({ statusCode: 500, body: {} }))
+ );
+ mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue(
+ securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } })
+ );
+
+ await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "correctiveActions": Object {
+ "manualSteps": Array [
+ "Check Kibana logs for more details.",
+ ],
+ },
+ "deprecationType": "feature",
+ "level": "fetch_error",
+ "message": "Failed to perform deprecation check. Check Kibana logs for more details.",
+ "title": "The \\"kibana_user\\" role is deprecated",
+ },
+ ]
+ `);
+ });
+
+ it('returns deprecations even if cannot retrieve role mappings due to permission error', async () => {
+ mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue(
+ securityMock.createApiResponse({ body: { userA: createMockUser() } })
+ );
+ mockContext.esClient.asCurrentUser.security.getRoleMapping.mockRejectedValue(
+ new errors.ResponseError(securityMock.createApiResponse({ statusCode: 403, body: {} }))
+ );
+
+ await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "correctiveActions": Object {
+ "manualSteps": Array [
+ "Make sure you have a \\"manage_security\\" cluster privilege assigned.",
+ ],
+ },
+ "deprecationType": "feature",
+ "documentationUrl": "https://www.elastic.co/guide/en/kibana/some-branch/xpack-security.html#_required_permissions_7",
+ "level": "fetch_error",
+ "message": "You do not have enough permissions to fix this deprecation.",
+ "title": "The \\"kibana_user\\" role is deprecated",
+ },
+ ]
+ `);
+ });
+
+ it('returns deprecations even if cannot retrieve role mappings due to unknown error', async () => {
+ mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue(
+ securityMock.createApiResponse({ body: { userA: createMockUser() } })
+ );
+ mockContext.esClient.asCurrentUser.security.getRoleMapping.mockRejectedValue(
+ new errors.ResponseError(securityMock.createApiResponse({ statusCode: 500, body: {} }))
+ );
+
+ await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "correctiveActions": Object {
+ "manualSteps": Array [
+ "Check Kibana logs for more details.",
+ ],
+ },
+ "deprecationType": "feature",
+ "level": "fetch_error",
+ "message": "Failed to perform deprecation check. Check Kibana logs for more details.",
+ "title": "The \\"kibana_user\\" role is deprecated",
+ },
+ ]
+ `);
+ });
+
+ it('returns only user-related deprecations', async () => {
+ mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ userA: createMockUser({ username: 'userA', roles: ['roleA'] }),
+ userB: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_user'] }),
+ userC: createMockUser({ username: 'userC', roles: ['roleC'] }),
+ userD: createMockUser({ username: 'userD', roles: ['kibana_user'] }),
+ },
+ })
+ );
+
+ mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue(
+ securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } })
+ );
+
+ await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "correctiveActions": Object {
+ "api": Object {
+ "method": "POST",
+ "path": "/internal/security/deprecations/kibana_user_role/_fix_users",
+ },
+ "manualSteps": Array [
+ "Remove the \\"kibana_user\\" role from all users and add the \\"kibana_admin\\" role. The affected users are: userB, userD.",
+ ],
+ },
+ "deprecationType": "feature",
+ "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html",
+ "level": "warning",
+ "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.",
+ "title": "The \\"kibana_user\\" role is deprecated",
+ },
+ ]
+ `);
+ });
+
+ it('returns only role-mapping-related deprecations', async () => {
+ mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue(
+ securityMock.createApiResponse({ body: { userA: createMockUser() } })
+ );
+
+ mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ mappingA: createMockRoleMapping({ roles: ['roleA'] }),
+ mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_user'] }),
+ mappingC: createMockRoleMapping({ roles: ['roleC'] }),
+ mappingD: createMockRoleMapping({ roles: ['kibana_user'] }),
+ },
+ })
+ );
+
+ await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "correctiveActions": Object {
+ "api": Object {
+ "method": "POST",
+ "path": "/internal/security/deprecations/kibana_user_role/_fix_role_mappings",
+ },
+ "manualSteps": Array [
+ "Remove the \\"kibana_user\\" role from all role mappings and add the \\"kibana_admin\\" role. The affected role mappings are: mappingB, mappingD.",
+ ],
+ },
+ "deprecationType": "feature",
+ "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html",
+ "level": "warning",
+ "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.",
+ "title": "The \\"kibana_user\\" role is deprecated",
+ },
+ ]
+ `);
+ });
+
+ it('returns both user-related and role-mapping-related deprecations', async () => {
+ mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ userA: createMockUser({ username: 'userA', roles: ['roleA'] }),
+ userB: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_user'] }),
+ userC: createMockUser({ username: 'userC', roles: ['roleC'] }),
+ userD: createMockUser({ username: 'userD', roles: ['kibana_user'] }),
+ },
+ })
+ );
+
+ mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ mappingA: createMockRoleMapping({ roles: ['roleA'] }),
+ mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_user'] }),
+ mappingC: createMockRoleMapping({ roles: ['roleC'] }),
+ mappingD: createMockRoleMapping({ roles: ['kibana_user'] }),
+ },
+ })
+ );
+
+ await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "correctiveActions": Object {
+ "api": Object {
+ "method": "POST",
+ "path": "/internal/security/deprecations/kibana_user_role/_fix_users",
+ },
+ "manualSteps": Array [
+ "Remove the \\"kibana_user\\" role from all users and add the \\"kibana_admin\\" role. The affected users are: userB, userD.",
+ ],
+ },
+ "deprecationType": "feature",
+ "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html",
+ "level": "warning",
+ "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.",
+ "title": "The \\"kibana_user\\" role is deprecated",
+ },
+ Object {
+ "correctiveActions": Object {
+ "api": Object {
+ "method": "POST",
+ "path": "/internal/security/deprecations/kibana_user_role/_fix_role_mappings",
+ },
+ "manualSteps": Array [
+ "Remove the \\"kibana_user\\" role from all role mappings and add the \\"kibana_admin\\" role. The affected role mappings are: mappingB, mappingD.",
+ ],
+ },
+ "deprecationType": "feature",
+ "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html",
+ "level": "warning",
+ "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.",
+ "title": "The \\"kibana_user\\" role is deprecated",
+ },
+ ]
+ `);
+ });
+});
diff --git a/x-pack/plugins/security/server/deprecations/kibana_user_role.ts b/x-pack/plugins/security/server/deprecations/kibana_user_role.ts
new file mode 100644
index 0000000000000..d659ea273f05f
--- /dev/null
+++ b/x-pack/plugins/security/server/deprecations/kibana_user_role.ts
@@ -0,0 +1,238 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type {
+ SecurityGetRoleMappingResponse,
+ SecurityGetUserResponse,
+} from '@elastic/elasticsearch/api/types';
+
+import { i18n } from '@kbn/i18n';
+import type {
+ DeprecationsDetails,
+ DeprecationsServiceSetup,
+ ElasticsearchClient,
+ Logger,
+ PackageInfo,
+} from 'src/core/server';
+
+import type { SecurityLicense } from '../../common';
+import { getDetailedErrorMessage, getErrorStatusCode } from '../errors';
+
+export const KIBANA_USER_ROLE_NAME = 'kibana_user';
+export const KIBANA_ADMIN_ROLE_NAME = 'kibana_admin';
+
+interface Deps {
+ deprecationsService: DeprecationsServiceSetup;
+ license: SecurityLicense;
+ logger: Logger;
+ packageInfo: PackageInfo;
+}
+
+function getDeprecationTitle() {
+ return i18n.translate('xpack.security.deprecations.kibanaUser.deprecationTitle', {
+ defaultMessage: 'The "{userRoleName}" role is deprecated',
+ values: { userRoleName: KIBANA_USER_ROLE_NAME },
+ });
+}
+
+function getDeprecationMessage() {
+ return i18n.translate('xpack.security.deprecations.kibanaUser.deprecationMessage', {
+ defaultMessage:
+ 'Use the "{adminRoleName}" role to grant access to all Kibana features in all spaces.',
+ values: { adminRoleName: KIBANA_ADMIN_ROLE_NAME },
+ });
+}
+
+export const registerKibanaUserRoleDeprecation = ({
+ deprecationsService,
+ logger,
+ license,
+ packageInfo,
+}: Deps) => {
+ deprecationsService.registerDeprecations({
+ getDeprecations: async (context) => {
+ // Nothing to do if security is disabled
+ if (!license.isEnabled()) {
+ return [];
+ }
+
+ return [
+ ...(await getUsersDeprecations(context.esClient.asCurrentUser, logger, packageInfo)),
+ ...(await getRoleMappingsDeprecations(context.esClient.asCurrentUser, logger, packageInfo)),
+ ];
+ },
+ });
+};
+
+async function getUsersDeprecations(
+ client: ElasticsearchClient,
+ logger: Logger,
+ packageInfo: PackageInfo
+): Promise {
+ let users: SecurityGetUserResponse;
+ try {
+ users = (await client.security.getUser()).body;
+ } catch (err) {
+ if (getErrorStatusCode(err) === 403) {
+ logger.warn(
+ `Failed to retrieve users when checking for deprecations: the "manage_security" cluster privilege is required.`
+ );
+ } else {
+ logger.error(
+ `Failed to retrieve users when checking for deprecations, unexpected error: ${getDetailedErrorMessage(
+ err
+ )}.`
+ );
+ }
+ return deprecationError(packageInfo, err);
+ }
+
+ const usersWithKibanaUserRole = Object.values(users)
+ .filter((user) => user.roles.includes(KIBANA_USER_ROLE_NAME))
+ .map((user) => user.username);
+ if (usersWithKibanaUserRole.length === 0) {
+ return [];
+ }
+
+ return [
+ {
+ title: getDeprecationTitle(),
+ message: getDeprecationMessage(),
+ level: 'warning',
+ deprecationType: 'feature',
+ documentationUrl: `https://www.elastic.co/guide/en/elasticsearch/reference/${packageInfo.branch}/built-in-roles.html`,
+ correctiveActions: {
+ api: {
+ method: 'POST',
+ path: '/internal/security/deprecations/kibana_user_role/_fix_users',
+ },
+ manualSteps: [
+ i18n.translate(
+ 'xpack.security.deprecations.kibanaUser.usersDeprecationCorrectiveAction',
+ {
+ defaultMessage:
+ 'Remove the "{userRoleName}" role from all users and add the "{adminRoleName}" role. The affected users are: {users}.',
+ values: {
+ userRoleName: KIBANA_USER_ROLE_NAME,
+ adminRoleName: KIBANA_ADMIN_ROLE_NAME,
+ users: usersWithKibanaUserRole.join(', '),
+ },
+ }
+ ),
+ ],
+ },
+ },
+ ];
+}
+
+async function getRoleMappingsDeprecations(
+ client: ElasticsearchClient,
+ logger: Logger,
+ packageInfo: PackageInfo
+): Promise {
+ let roleMappings: SecurityGetRoleMappingResponse;
+ try {
+ roleMappings = (await client.security.getRoleMapping()).body;
+ } catch (err) {
+ if (getErrorStatusCode(err) === 403) {
+ logger.warn(
+ `Failed to retrieve role mappings when checking for deprecations: the "manage_security" cluster privilege is required.`
+ );
+ } else {
+ logger.error(
+ `Failed to retrieve role mappings when checking for deprecations, unexpected error: ${getDetailedErrorMessage(
+ err
+ )}.`
+ );
+ }
+ return deprecationError(packageInfo, err);
+ }
+
+ const roleMappingsWithKibanaUserRole = Object.entries(roleMappings)
+ .filter(([, roleMapping]) => roleMapping.roles.includes(KIBANA_USER_ROLE_NAME))
+ .map(([mappingName]) => mappingName);
+ if (roleMappingsWithKibanaUserRole.length === 0) {
+ return [];
+ }
+
+ return [
+ {
+ title: getDeprecationTitle(),
+ message: getDeprecationMessage(),
+ level: 'warning',
+ deprecationType: 'feature',
+ documentationUrl: `https://www.elastic.co/guide/en/elasticsearch/reference/${packageInfo.branch}/built-in-roles.html`,
+ correctiveActions: {
+ api: {
+ method: 'POST',
+ path: '/internal/security/deprecations/kibana_user_role/_fix_role_mappings',
+ },
+ manualSteps: [
+ i18n.translate(
+ 'xpack.security.deprecations.kibanaUser.roleMappingsDeprecationCorrectiveAction',
+ {
+ defaultMessage:
+ 'Remove the "{userRoleName}" role from all role mappings and add the "{adminRoleName}" role. The affected role mappings are: {roleMappings}.',
+ values: {
+ userRoleName: KIBANA_USER_ROLE_NAME,
+ adminRoleName: KIBANA_ADMIN_ROLE_NAME,
+ roleMappings: roleMappingsWithKibanaUserRole.join(', '),
+ },
+ }
+ ),
+ ],
+ },
+ },
+ ];
+}
+
+function deprecationError(packageInfo: PackageInfo, error: Error): DeprecationsDetails[] {
+ const title = getDeprecationTitle();
+
+ if (getErrorStatusCode(error) === 403) {
+ return [
+ {
+ title,
+ level: 'fetch_error',
+ deprecationType: 'feature',
+ message: i18n.translate('xpack.security.deprecations.kibanaUser.forbiddenErrorMessage', {
+ defaultMessage: 'You do not have enough permissions to fix this deprecation.',
+ }),
+ documentationUrl: `https://www.elastic.co/guide/en/kibana/${packageInfo.branch}/xpack-security.html#_required_permissions_7`,
+ correctiveActions: {
+ manualSteps: [
+ i18n.translate(
+ 'xpack.security.deprecations.kibanaUser.forbiddenErrorCorrectiveAction',
+ {
+ defaultMessage:
+ 'Make sure you have a "manage_security" cluster privilege assigned.',
+ }
+ ),
+ ],
+ },
+ },
+ ];
+ }
+
+ return [
+ {
+ title,
+ level: 'fetch_error',
+ deprecationType: 'feature',
+ message: i18n.translate('xpack.security.deprecations.kibanaUser.unknownErrorMessage', {
+ defaultMessage: 'Failed to perform deprecation check. Check Kibana logs for more details.',
+ }),
+ correctiveActions: {
+ manualSteps: [
+ i18n.translate('xpack.security.deprecations.kibanaUser.unknownErrorCorrectiveAction', {
+ defaultMessage: 'Check Kibana logs for more details.',
+ }),
+ ],
+ },
+ },
+ ];
+}
diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts
index 98e77038f168a..1e42d10b205aa 100644
--- a/x-pack/plugins/security/server/plugin.ts
+++ b/x-pack/plugins/security/server/plugin.ts
@@ -27,9 +27,8 @@ import type {
import type { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
import type { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server';
import type { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
-import type { SecurityLicense } from '../common/licensing';
+import type { AuthenticatedUser, PrivilegeDeprecationsService, SecurityLicense } from '../common';
import { SecurityLicenseService } from '../common/licensing';
-import type { AuthenticatedUser, PrivilegeDeprecationsService } from '../common/model';
import type { AnonymousAccessServiceStart } from './anonymous_access';
import { AnonymousAccessService } from './anonymous_access';
import type { AuditServiceSetup } from './audit';
@@ -43,7 +42,7 @@ import type { AuthorizationServiceSetup, AuthorizationServiceSetupInternal } fro
import { AuthorizationService } from './authorization';
import type { ConfigSchema, ConfigType } from './config';
import { createConfig } from './config';
-import { getPrivilegeDeprecationsService } from './deprecations';
+import { getPrivilegeDeprecationsService, registerKibanaUserRoleDeprecation } from './deprecations';
import { ElasticsearchService } from './elasticsearch';
import type { SecurityFeatureUsageServiceStart } from './feature_usage';
import { SecurityFeatureUsageService } from './feature_usage';
@@ -290,6 +289,8 @@ export class SecurityPlugin
getSpacesService: () => spaces?.spacesService,
});
+ this.registerDeprecations(core, license);
+
defineRoutes({
router: core.http.createRouter(),
basePath: core.http.basePath,
@@ -414,4 +415,14 @@ export class SecurityPlugin
this.authorizationService.stop();
this.sessionManagementService.stop();
}
+
+ private registerDeprecations(core: CoreSetup, license: SecurityLicense) {
+ const logger = this.logger.get('deprecations');
+ registerKibanaUserRoleDeprecation({
+ deprecationsService: core.deprecations,
+ license,
+ logger,
+ packageInfo: this.initializerContext.env.packageInfo,
+ });
+ }
}
diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/index.ts b/x-pack/plugins/security/server/routes/deprecations/index.ts
similarity index 50%
rename from x-pack/plugins/monitoring/public/application/setup_mode/index.ts
rename to x-pack/plugins/security/server/routes/deprecations/index.ts
index 57d734fc6d056..cbc186ed2e925 100644
--- a/x-pack/plugins/monitoring/public/application/setup_mode/index.ts
+++ b/x-pack/plugins/security/server/routes/deprecations/index.ts
@@ -5,4 +5,9 @@
* 2.0.
*/
-export * from '../../lib/setup_mode';
+import type { RouteDefinitionParams } from '../';
+import { defineKibanaUserRoleDeprecationRoutes } from './kibana_user_role';
+
+export function defineDeprecationsRoutes(params: RouteDefinitionParams) {
+ defineKibanaUserRoleDeprecationRoutes(params);
+}
diff --git a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts
new file mode 100644
index 0000000000000..b2ae2543bd652
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts
@@ -0,0 +1,283 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { errors } from '@elastic/elasticsearch';
+import type { SecurityRoleMapping, SecurityUser } from '@elastic/elasticsearch/api/types';
+
+import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
+import type { RequestHandler, RouteConfig } from 'src/core/server';
+import { kibanaResponseFactory } from 'src/core/server';
+import { coreMock, httpServerMock } from 'src/core/server/mocks';
+
+import { securityMock } from '../../mocks';
+import type { SecurityRequestHandlerContext, SecurityRouter } from '../../types';
+import { routeDefinitionParamsMock } from '../index.mock';
+import { defineKibanaUserRoleDeprecationRoutes } from './kibana_user_role';
+
+function createMockUser(user: Partial = {}) {
+ return { enabled: true, username: 'userA', roles: ['roleA'], metadata: {}, ...user };
+}
+
+function createMockRoleMapping(mapping: Partial = {}) {
+ return { enabled: true, roles: ['roleA'], rules: {}, metadata: {}, ...mapping };
+}
+
+describe('Kibana user deprecation routes', () => {
+ let router: jest.Mocked;
+ let mockContext: DeeplyMockedKeys;
+ beforeEach(() => {
+ const routeParamsMock = routeDefinitionParamsMock.create();
+ router = routeParamsMock.router;
+
+ mockContext = {
+ core: coreMock.createRequestHandlerContext(),
+ licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } },
+ } as any;
+
+ defineKibanaUserRoleDeprecationRoutes(routeParamsMock);
+ });
+
+ describe('Users with Kibana user role', () => {
+ let routeHandler: RequestHandler;
+ let routeConfig: RouteConfig;
+ beforeEach(() => {
+ const [fixUsersRouteConfig, fixUsersRouteHandler] = router.post.mock.calls.find(
+ ([{ path }]) => path === '/internal/security/deprecations/kibana_user_role/_fix_users'
+ )!;
+
+ routeConfig = fixUsersRouteConfig;
+ routeHandler = fixUsersRouteHandler;
+ });
+
+ it('correctly defines route.', () => {
+ expect(routeConfig.options).toBeUndefined();
+ expect(routeConfig.validate).toBe(false);
+ });
+
+ it('fails if cannot retrieve users', async () => {
+ mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockRejectedValue(
+ new errors.ResponseError(
+ securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') })
+ )
+ );
+
+ await expect(
+ routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory)
+ ).resolves.toEqual(expect.objectContaining({ status: 500 }));
+
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putUser
+ ).not.toHaveBeenCalled();
+ });
+
+ it('fails if fails to update user', async () => {
+ mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ userA: createMockUser({ username: 'userA', roles: ['roleA', 'kibana_user'] }),
+ userB: createMockUser({ username: 'userB', roles: ['kibana_user'] }),
+ },
+ })
+ );
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putUser.mockRejectedValue(
+ new errors.ResponseError(
+ securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') })
+ )
+ );
+
+ await expect(
+ routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory)
+ ).resolves.toEqual(expect.objectContaining({ status: 500 }));
+
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putUser
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putUser
+ ).toHaveBeenCalledWith({
+ username: 'userA',
+ body: createMockUser({ username: 'userA', roles: ['roleA', 'kibana_admin'] }),
+ });
+ });
+
+ it('does nothing if there are no users with Kibana user role', async () => {
+ mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockResolvedValue(
+ securityMock.createApiResponse({ body: { userA: createMockUser() } })
+ );
+
+ await expect(
+ routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory)
+ ).resolves.toEqual(expect.objectContaining({ status: 200 }));
+
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putUser
+ ).not.toHaveBeenCalled();
+ });
+
+ it('updates users with Kibana user role', async () => {
+ mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ userA: createMockUser({ username: 'userA', roles: ['roleA'] }),
+ userB: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_user'] }),
+ userC: createMockUser({ username: 'userC', roles: ['roleC'] }),
+ userD: createMockUser({ username: 'userD', roles: ['kibana_user'] }),
+ userE: createMockUser({
+ username: 'userE',
+ roles: ['kibana_user', 'kibana_admin', 'roleE'],
+ }),
+ },
+ })
+ );
+
+ await expect(
+ routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory)
+ ).resolves.toEqual(expect.objectContaining({ status: 200 }));
+
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putUser
+ ).toHaveBeenCalledTimes(3);
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putUser
+ ).toHaveBeenCalledWith({
+ username: 'userB',
+ body: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_admin'] }),
+ });
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putUser
+ ).toHaveBeenCalledWith({
+ username: 'userD',
+ body: createMockUser({ username: 'userD', roles: ['kibana_admin'] }),
+ });
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putUser
+ ).toHaveBeenCalledWith({
+ username: 'userE',
+ body: createMockUser({ username: 'userE', roles: ['kibana_admin', 'roleE'] }),
+ });
+ });
+ });
+
+ describe('Role mappings with Kibana user role', () => {
+ let routeHandler: RequestHandler;
+ let routeConfig: RouteConfig;
+ beforeEach(() => {
+ const [fixRoleMappingsRouteConfig, fixRoleMappingsRouteHandler] = router.post.mock.calls.find(
+ ([{ path }]) =>
+ path === '/internal/security/deprecations/kibana_user_role/_fix_role_mappings'
+ )!;
+
+ routeConfig = fixRoleMappingsRouteConfig;
+ routeHandler = fixRoleMappingsRouteHandler;
+ });
+
+ it('correctly defines route.', () => {
+ expect(routeConfig.options).toBeUndefined();
+ expect(routeConfig.validate).toBe(false);
+ });
+
+ it('fails if cannot retrieve role mappings', async () => {
+ mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockRejectedValue(
+ new errors.ResponseError(
+ securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') })
+ )
+ );
+
+ await expect(
+ routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory)
+ ).resolves.toEqual(expect.objectContaining({ status: 500 }));
+
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping
+ ).not.toHaveBeenCalled();
+ });
+
+ it('fails if fails to update role mapping', async () => {
+ mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ mappingA: createMockRoleMapping({ roles: ['roleA', 'kibana_user'] }),
+ mappingB: createMockRoleMapping({ roles: ['kibana_user'] }),
+ },
+ })
+ );
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping.mockRejectedValue(
+ new errors.ResponseError(
+ securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') })
+ )
+ );
+
+ await expect(
+ routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory)
+ ).resolves.toEqual(expect.objectContaining({ status: 500 }));
+
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping
+ ).toHaveBeenCalledWith({
+ name: 'mappingA',
+ body: createMockRoleMapping({ roles: ['roleA', 'kibana_admin'] }),
+ });
+ });
+
+ it('does nothing if there are no role mappings with Kibana user role', async () => {
+ mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue(
+ securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } })
+ );
+
+ await expect(
+ routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory)
+ ).resolves.toEqual(expect.objectContaining({ status: 200 }));
+
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping
+ ).not.toHaveBeenCalled();
+ });
+
+ it('updates role mappings with Kibana user role', async () => {
+ mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ mappingA: createMockRoleMapping({ roles: ['roleA'] }),
+ mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_user'] }),
+ mappingC: createMockRoleMapping({ roles: ['roleC'] }),
+ mappingD: createMockRoleMapping({ roles: ['kibana_user'] }),
+ mappingE: createMockRoleMapping({ roles: ['kibana_user', 'kibana_admin', 'roleE'] }),
+ },
+ })
+ );
+
+ await expect(
+ routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory)
+ ).resolves.toEqual(expect.objectContaining({ status: 200 }));
+
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping
+ ).toHaveBeenCalledTimes(3);
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping
+ ).toHaveBeenCalledWith({
+ name: 'mappingB',
+ body: createMockRoleMapping({ roles: ['roleB', 'kibana_admin'] }),
+ });
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping
+ ).toHaveBeenCalledWith({
+ name: 'mappingD',
+ body: createMockRoleMapping({ roles: ['kibana_admin'] }),
+ });
+ expect(
+ mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping
+ ).toHaveBeenCalledWith({
+ name: 'mappingE',
+ body: createMockRoleMapping({ roles: ['kibana_admin', 'roleE'] }),
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts
new file mode 100644
index 0000000000000..21bb9db7329b6
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts
@@ -0,0 +1,145 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type {
+ SecurityGetRoleMappingResponse,
+ SecurityGetUserResponse,
+} from '@elastic/elasticsearch/api/types';
+
+import type { RouteDefinitionParams } from '..';
+import { KIBANA_ADMIN_ROLE_NAME, KIBANA_USER_ROLE_NAME } from '../../deprecations';
+import {
+ getDetailedErrorMessage,
+ getErrorStatusCode,
+ wrapIntoCustomErrorResponse,
+} from '../../errors';
+import { createLicensedRouteHandler } from '../licensed_route_handler';
+
+/**
+ * Defines routes required to handle `kibana_user` deprecation.
+ */
+export function defineKibanaUserRoleDeprecationRoutes({ router, logger }: RouteDefinitionParams) {
+ router.post(
+ {
+ path: '/internal/security/deprecations/kibana_user_role/_fix_users',
+ validate: false,
+ },
+ createLicensedRouteHandler(async (context, request, response) => {
+ let users: SecurityGetUserResponse;
+ try {
+ users = (await context.core.elasticsearch.client.asCurrentUser.security.getUser()).body;
+ } catch (err) {
+ if (getErrorStatusCode(err) === 403) {
+ logger.warn(
+ `Failed to retrieve users when checking for deprecations: the manage_security cluster privilege is required`
+ );
+ } else {
+ logger.error(
+ `Failed to retrieve users when checking for deprecations, unexpected error: ${getDetailedErrorMessage(
+ err
+ )}`
+ );
+ }
+ return response.customError(wrapIntoCustomErrorResponse(err));
+ }
+
+ const usersWithKibanaUserRole = Object.values(users).filter((user) =>
+ user.roles.includes(KIBANA_USER_ROLE_NAME)
+ );
+
+ if (usersWithKibanaUserRole.length === 0) {
+ logger.debug(`No users with "${KIBANA_USER_ROLE_NAME}" role found.`);
+ } else {
+ logger.debug(
+ `The following users with "${KIBANA_USER_ROLE_NAME}" role found and will be migrated to "${KIBANA_ADMIN_ROLE_NAME}" role: ${usersWithKibanaUserRole
+ .map((user) => user.username)
+ .join(', ')}.`
+ );
+ }
+
+ for (const userToUpdate of usersWithKibanaUserRole) {
+ const roles = userToUpdate.roles.filter((role) => role !== KIBANA_USER_ROLE_NAME);
+ if (!roles.includes(KIBANA_ADMIN_ROLE_NAME)) {
+ roles.push(KIBANA_ADMIN_ROLE_NAME);
+ }
+
+ try {
+ await context.core.elasticsearch.client.asCurrentUser.security.putUser({
+ username: userToUpdate.username,
+ body: { ...userToUpdate, roles },
+ });
+ } catch (err) {
+ logger.error(
+ `Failed to update user "${userToUpdate.username}": ${getDetailedErrorMessage(err)}.`
+ );
+ return response.customError(wrapIntoCustomErrorResponse(err));
+ }
+
+ logger.debug(`Successfully updated user "${userToUpdate.username}".`);
+ }
+
+ return response.ok({ body: {} });
+ })
+ );
+
+ router.post(
+ {
+ path: '/internal/security/deprecations/kibana_user_role/_fix_role_mappings',
+ validate: false,
+ },
+ createLicensedRouteHandler(async (context, request, response) => {
+ let roleMappings: SecurityGetRoleMappingResponse;
+ try {
+ roleMappings = (
+ await context.core.elasticsearch.client.asCurrentUser.security.getRoleMapping()
+ ).body;
+ } catch (err) {
+ logger.error(`Failed to retrieve role mappings: ${getDetailedErrorMessage(err)}.`);
+ return response.customError(wrapIntoCustomErrorResponse(err));
+ }
+
+ const roleMappingsWithKibanaUserRole = Object.entries(roleMappings).filter(([, mapping]) =>
+ mapping.roles.includes(KIBANA_USER_ROLE_NAME)
+ );
+
+ if (roleMappingsWithKibanaUserRole.length === 0) {
+ logger.debug(`No role mappings with "${KIBANA_USER_ROLE_NAME}" role found.`);
+ } else {
+ logger.debug(
+ `The following role mappings with "${KIBANA_USER_ROLE_NAME}" role found and will be migrated to "${KIBANA_ADMIN_ROLE_NAME}" role: ${roleMappingsWithKibanaUserRole
+ .map(([mappingName]) => mappingName)
+ .join(', ')}.`
+ );
+ }
+
+ for (const [mappingNameToUpdate, mappingToUpdate] of roleMappingsWithKibanaUserRole) {
+ const roles = mappingToUpdate.roles.filter((role) => role !== KIBANA_USER_ROLE_NAME);
+ if (!roles.includes(KIBANA_ADMIN_ROLE_NAME)) {
+ roles.push(KIBANA_ADMIN_ROLE_NAME);
+ }
+
+ try {
+ await context.core.elasticsearch.client.asCurrentUser.security.putRoleMapping({
+ name: mappingNameToUpdate,
+ body: { ...mappingToUpdate, roles },
+ });
+ } catch (err) {
+ logger.error(
+ `Failed to update role mapping "${mappingNameToUpdate}": ${getDetailedErrorMessage(
+ err
+ )}.`
+ );
+ return response.customError(wrapIntoCustomErrorResponse(err));
+ }
+
+ logger.debug(`Successfully updated role mapping "${mappingNameToUpdate}".`);
+ }
+
+ return response.ok({ body: {} });
+ })
+ );
+}
diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts
index 851e70a357cf9..6785fe57c6b32 100644
--- a/x-pack/plugins/security/server/routes/index.ts
+++ b/x-pack/plugins/security/server/routes/index.ts
@@ -11,7 +11,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types';
import type { HttpResources, IBasePath, Logger } from 'src/core/server';
import type { KibanaFeature } from '../../../features/server';
-import type { SecurityLicense } from '../../common/licensing';
+import type { SecurityLicense } from '../../common';
import type { AnonymousAccessServiceStart } from '../anonymous_access';
import type { InternalAuthenticationServiceStart } from '../authentication';
import type { AuthorizationServiceSetupInternal } from '../authorization';
@@ -23,6 +23,7 @@ import { defineAnonymousAccessRoutes } from './anonymous_access';
import { defineApiKeysRoutes } from './api_keys';
import { defineAuthenticationRoutes } from './authentication';
import { defineAuthorizationRoutes } from './authorization';
+import { defineDeprecationsRoutes } from './deprecations';
import { defineIndicesRoutes } from './indices';
import { defineRoleMappingRoutes } from './role_mapping';
import { defineSecurityCheckupGetStateRoutes } from './security_checkup';
@@ -58,6 +59,7 @@ export function defineRoutes(params: RouteDefinitionParams) {
defineUsersRoutes(params);
defineRoleMappingRoutes(params);
defineViewRoutes(params);
+ defineDeprecationsRoutes(params);
defineAnonymousAccessRoutes(params);
defineSecurityCheckupGetStateRoutes(params);
}
diff --git a/x-pack/plugins/security_solution/cypress/downloads/test_exception_list.ndjson b/x-pack/plugins/security_solution/cypress/downloads/test_exception_list.ndjson
deleted file mode 100644
index 54420eff29e0d..0000000000000
--- a/x-pack/plugins/security_solution/cypress/downloads/test_exception_list.ndjson
+++ /dev/null
@@ -1,2 +0,0 @@
-{"_version":"WzQyNjA0LDFd","created_at":"2021-10-14T01:30:22.034Z","created_by":"elastic","description":"Test exception list description","id":"4c65a230-2c8e-11ec-be1c-2bbdec602f88","immutable":false,"list_id":"test_exception_list","name":"Test exception list","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"b04983b4-1617-441c-bb6c-c729281fa2e9","type":"detection","updated_at":"2021-10-14T01:30:22.036Z","updated_by":"elastic","version":1}
-{"exported_list_items_count":0}
diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts
index 69b623de0b43c..287d86c6fba9e 100644
--- a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts
@@ -20,8 +20,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { CASES_URL } from '../../urls/navigation';
-// Skipping flakey test: https://github.com/elastic/kibana/issues/115438
-describe.skip('Cases connectors', () => {
+describe('Cases connectors', () => {
const configureResult = {
connector: {
id: 'e271c3b8-f702-4fbc-98e0-db942b573bbd',
diff --git a/x-pack/plugins/security_solution/cypress/objects/exception.ts b/x-pack/plugins/security_solution/cypress/objects/exception.ts
index b772924697148..1a70bb1038320 100644
--- a/x-pack/plugins/security_solution/cypress/objects/exception.ts
+++ b/x-pack/plugins/security_solution/cypress/objects/exception.ts
@@ -41,5 +41,5 @@ export const expectedExportedExceptionList = (
exceptionListResponse: Cypress.Response
): string => {
const jsonrule = exceptionListResponse.body;
- return `{"_version":"${jsonrule._version}","created_at":"${jsonrule.created_at}","created_by":"elastic","description":"${jsonrule.description}","id":"${jsonrule.id}","immutable":false,"list_id":"test_exception_list","name":"Test exception list","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonrule.tie_breaker_id}","type":"detection","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","version":1}\n{"exported_list_items_count":0}\n`;
+ return `{"_version":"${jsonrule._version}","created_at":"${jsonrule.created_at}","created_by":"elastic","description":"${jsonrule.description}","id":"${jsonrule.id}","immutable":false,"list_id":"test_exception_list","name":"Test exception list","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonrule.tie_breaker_id}","type":"detection","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","version":1}\n{"exported_exception_list_count":1,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n`;
};
diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts
index 27973854097db..ae04e20dfe86e 100644
--- a/x-pack/plugins/security_solution/cypress/objects/rule.ts
+++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts
@@ -421,5 +421,5 @@ export const getEditedRule = (): CustomRule => ({
export const expectedExportedRule = (ruleResponse: Cypress.Response): string => {
const jsonrule = ruleResponse.body;
- return `{"id":"${jsonrule.id}","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","created_at":"${jsonrule.created_at}","created_by":"elastic","name":"${jsonrule.name}","tags":[],"interval":"100m","enabled":false,"description":"${jsonrule.description}","risk_score":${jsonrule.risk_score},"severity":"${jsonrule.severity}","output_index":".siem-signals-default","author":[],"false_positives":[],"from":"now-50000h","rule_id":"rule_testing","max_signals":100,"risk_score_mapping":[],"severity_mapping":[],"threat":[],"to":"now","references":[],"version":1,"exceptions_list":[],"immutable":false,"type":"query","language":"kuery","index":["exceptions-*"],"query":"${jsonrule.query}","throttle":"no_actions","actions":[]}\n{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n`;
+ return `{"id":"${jsonrule.id}","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","created_at":"${jsonrule.created_at}","created_by":"elastic","name":"${jsonrule.name}","tags":[],"interval":"100m","enabled":false,"description":"${jsonrule.description}","risk_score":${jsonrule.risk_score},"severity":"${jsonrule.severity}","output_index":".siem-signals-default","author":[],"false_positives":[],"from":"now-50000h","rule_id":"rule_testing","max_signals":100,"risk_score_mapping":[],"severity_mapping":[],"threat":[],"to":"now","references":[],"version":1,"exceptions_list":[],"immutable":false,"type":"query","language":"kuery","index":["exceptions-*"],"query":"${jsonrule.query}","throttle":"no_actions","actions":[]}\n{"exported_rules_count":1,"missing_rules":[],"missing_rules_count":0,"exported_exception_list_count":0,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n`;
};
diff --git a/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts b/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts
index 1014835f81efe..c9ed5299c0336 100644
--- a/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts
@@ -24,7 +24,7 @@ export const SERVICE_NOW_CONNECTOR_CARD = '[data-test-subj=".servicenow-card"]';
export const TOASTER = '[data-test-subj="euiToastHeader"]';
-export const URL = '[data-test-subj="apiUrlFromInput"]';
+export const URL = '[data-test-subj="credentialsApiUrlFromInput"]';
export const USERNAME = '[data-test-subj="connector-servicenow-username-form-input"]';
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.tsx
index d09d33ce5aded..2d3e6dcdca4c5 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.tsx
@@ -12,6 +12,7 @@ import {
Position,
Settings,
ChartSizeArray,
+ ScaleType,
} from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui';
import React, { useMemo } from 'react';
@@ -42,7 +43,7 @@ export const AlertsHistogram = React.memo(
data,
from,
legendItems,
- legendPosition = 'right',
+ legendPosition = Position.Right,
loading,
showLegend,
to,
@@ -81,14 +82,14 @@ export const AlertsHistogram = React.memo(
theme={theme}
/>
-
+
-
+
{
const siemResponse = buildSiemResponse(response);
const rulesClient = context.alerting?.getRulesClient();
+ const exceptionsClient = context.lists?.getExceptionListClient();
const savedObjectsClient = context.core.savedObjects.client;
if (!rulesClient) {
@@ -72,20 +73,27 @@ export const exportRulesRoute = (
}
}
- const exported =
+ const exportedRulesAndExceptions =
request.body?.objects != null
? await getExportByObjectIds(
rulesClient,
+ exceptionsClient,
savedObjectsClient,
request.body.objects,
logger,
isRuleRegistryEnabled
)
- : await getExportAll(rulesClient, savedObjectsClient, logger, isRuleRegistryEnabled);
+ : await getExportAll(
+ rulesClient,
+ exceptionsClient,
+ savedObjectsClient,
+ logger,
+ isRuleRegistryEnabled
+ );
const responseBody = request.query.exclude_export_details
- ? exported.rulesNdjson
- : `${exported.rulesNdjson}${exported.exportDetails}`;
+ ? exportedRulesAndExceptions.rulesNdjson
+ : `${exportedRulesAndExceptions.rulesNdjson}${exportedRulesAndExceptions.exceptionLists}${exportedRulesAndExceptions.exportDetails}`;
return response.ok({
headers: {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts
index fb5a2315479da..d043149f8474e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts
@@ -47,6 +47,7 @@ export const performBulkActionRoute = (
try {
const rulesClient = context.alerting?.getRulesClient();
+ const exceptionsClient = context.lists?.getExceptionListClient();
const savedObjectsClient = context.core.savedObjects.client;
const ruleStatusClient = context.securitySolution.getExecutionLogClient();
@@ -136,13 +137,14 @@ export const performBulkActionRoute = (
case BulkAction.export:
const exported = await getExportByObjectIds(
rulesClient,
+ exceptionsClient,
savedObjectsClient,
rules.data.map(({ params }) => ({ rule_id: params.ruleId })),
logger,
isRuleRegistryEnabled
);
- const responseBody = `${exported.rulesNdjson}${exported.exportDetails}`;
+ const responseBody = `${exported.rulesNdjson}${exported.exceptionLists}${exported.exportDetails}`;
return response.ok({
headers: {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts
index c5b3e98c4c44e..f56d1d83eb873 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts
@@ -207,7 +207,7 @@ describe('create_rules_stream_from_ndjson', () => {
read() {
this.push(getSampleAsNdjson(sample1));
this.push(getSampleAsNdjson(sample2));
- this.push('{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n');
+ this.push('{"exported_rules_count":1,"missing_rules":[],"missing_rules_count":0}\n');
this.push(null);
},
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts
index 0c2d81c18646b..d4357c45fd373 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts
@@ -21,7 +21,8 @@ import {
} from '../../../../common/detection_engine/schemas/request/import_rules_schema';
import {
parseNdjsonStrings,
- filterExportedCounts,
+ filterExportedRulesCounts,
+ filterExceptions,
createLimitStream,
} from '../../../utils/read_stream/create_stream_from_ndjson';
@@ -59,7 +60,8 @@ export const createRulesStreamFromNdJson = (ruleLimit: number) => {
return [
createSplitStream('\n'),
parseNdjsonStrings(),
- filterExportedCounts(),
+ filterExportedRulesCounts(),
+ filterExceptions(),
validateRules(),
createLimitStream(ruleLimit),
createConcatStream([]),
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts
index 92e4f0bbb4a5e..80df4c94971cc 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts
@@ -17,9 +17,12 @@ import { getListArrayMock } from '../../../../common/detection_engine/schemas/ty
import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock';
import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
+import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock';
import { loggingSystemMock } from 'src/core/server/mocks';
import { requestContextMock } from '../routes/__mocks__/request_context';
+const exceptionsClient = getExceptionListClientMock();
+
describe.each([
['Legacy', false],
['RAC', true],
@@ -49,6 +52,7 @@ describe.each([
const exports = await getExportAll(
rulesClient,
+ exceptionsClient,
clients.savedObjectsClient,
logger,
isRuleRegistryEnabled
@@ -97,7 +101,13 @@ describe.each([
exceptions_list: getListArrayMock(),
});
expect(detailsJson).toEqual({
- exported_count: 1,
+ exported_exception_list_count: 0,
+ exported_exception_list_item_count: 0,
+ exported_rules_count: 1,
+ missing_exception_list_item_count: 0,
+ missing_exception_list_items: [],
+ missing_exception_lists: [],
+ missing_exception_lists_count: 0,
missing_rules: [],
missing_rules_count: 0,
});
@@ -116,13 +126,16 @@ describe.each([
const exports = await getExportAll(
rulesClient,
+ exceptionsClient,
clients.savedObjectsClient,
logger,
isRuleRegistryEnabled
);
expect(exports).toEqual({
rulesNdjson: '',
- exportDetails: '{"exported_count":0,"missing_rules":[],"missing_rules_count":0}\n',
+ exportDetails:
+ '{"exported_rules_count":0,"missing_rules":[],"missing_rules_count":0,"exported_exception_list_count":0,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n',
+ exceptionLists: '',
});
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts
index cbbda5df7e2bf..c0389de766ea5 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts
@@ -8,35 +8,44 @@
import { transformDataToNdjson } from '@kbn/securitysolution-utils';
import { Logger } from 'src/core/server';
+import { ExceptionListClient } from '../../../../../lists/server';
import { RulesClient, AlertServices } from '../../../../../alerting/server';
import { getNonPackagedRules } from './get_existing_prepackaged_rules';
import { getExportDetailsNdjson } from './get_export_details_ndjson';
import { transformAlertsToRules } from '../routes/rules/utils';
+import { getRuleExceptionsForExport } from './get_export_rule_exceptions';
// eslint-disable-next-line no-restricted-imports
import { legacyGetBulkRuleActionsSavedObject } from '../rule_actions/legacy_get_bulk_rule_actions_saved_object';
export const getExportAll = async (
rulesClient: RulesClient,
+ exceptionsClient: ExceptionListClient | undefined,
savedObjectsClient: AlertServices['savedObjectsClient'],
logger: Logger,
isRuleRegistryEnabled: boolean
): Promise<{
rulesNdjson: string;
exportDetails: string;
+ exceptionLists: string | null;
}> => {
const ruleAlertTypes = await getNonPackagedRules({ rulesClient, isRuleRegistryEnabled });
const alertIds = ruleAlertTypes.map((rule) => rule.id);
+
+ // Gather actions
const legacyActions = await legacyGetBulkRuleActionsSavedObject({
alertIds,
savedObjectsClient,
logger,
});
-
const rules = transformAlertsToRules(ruleAlertTypes, legacyActions);
- // We do not support importing/exporting actions. When we do, delete this line of code
- const rulesWithoutActions = rules.map((rule) => ({ ...rule, actions: [] }));
- const rulesNdjson = transformDataToNdjson(rulesWithoutActions);
- const exportDetails = getExportDetailsNdjson(rules);
- return { rulesNdjson, exportDetails };
+
+ // Gather exceptions
+ const exceptions = rules.flatMap((rule) => rule.exceptions_list ?? []);
+ const { exportData: exceptionLists, exportDetails: exceptionDetails } =
+ await getRuleExceptionsForExport(exceptions, exceptionsClient);
+
+ const rulesNdjson = transformDataToNdjson(rules);
+ const exportDetails = getExportDetailsNdjson(rules, [], exceptionDetails);
+ return { rulesNdjson, exportDetails, exceptionLists };
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts
index 961f2c6a41866..7aa55a8163e1a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts
@@ -16,6 +16,9 @@ import { rulesClientMock } from '../../../../../alerting/server/mocks';
import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock';
import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock';
import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
+import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock';
+
+const exceptionsClient = getExceptionListClientMock();
import { loggingSystemMock } from 'src/core/server/mocks';
import { requestContextMock } from '../routes/__mocks__/request_context';
@@ -42,6 +45,7 @@ describe.each([
const objects = [{ rule_id: 'rule-1' }];
const exports = await getExportByObjectIds(
rulesClient,
+ exceptionsClient,
clients.savedObjectsClient,
objects,
logger,
@@ -94,7 +98,13 @@ describe.each([
exceptions_list: getListArrayMock(),
},
exportDetails: {
- exported_count: 1,
+ exported_exception_list_count: 0,
+ exported_exception_list_item_count: 0,
+ exported_rules_count: 1,
+ missing_exception_list_item_count: 0,
+ missing_exception_list_items: [],
+ missing_exception_lists: [],
+ missing_exception_lists_count: 0,
missing_rules: [],
missing_rules_count: 0,
},
@@ -119,6 +129,7 @@ describe.each([
const objects = [{ rule_id: 'rule-1' }];
const exports = await getExportByObjectIds(
rulesClient,
+ exceptionsClient,
clients.savedObjectsClient,
objects,
logger,
@@ -127,7 +138,8 @@ describe.each([
expect(exports).toEqual({
rulesNdjson: '',
exportDetails:
- '{"exported_count":0,"missing_rules":[{"rule_id":"rule-1"}],"missing_rules_count":1}\n',
+ '{"exported_rules_count":0,"missing_rules":[{"rule_id":"rule-1"}],"missing_rules_count":1,"exported_exception_list_count":0,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n',
+ exceptionLists: '',
});
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts
index 8233fe6d4948c..81295c9197644 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts
@@ -9,6 +9,7 @@ import { chunk } from 'lodash';
import { transformDataToNdjson } from '@kbn/securitysolution-utils';
import { Logger } from 'src/core/server';
+import { ExceptionListClient } from '../../../../../lists/server';
import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema';
import { RulesClient, AlertServices } from '../../../../../alerting/server';
@@ -18,6 +19,7 @@ import { isAlertType } from '../rules/types';
import { transformAlertToRule } from '../routes/rules/utils';
import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants';
import { findRules } from './find_rules';
+import { getRuleExceptionsForExport } from './get_export_rule_exceptions';
// eslint-disable-next-line no-restricted-imports
import { legacyGetBulkRuleActionsSavedObject } from '../rule_actions/legacy_get_bulk_rule_actions_saved_object';
@@ -40,6 +42,7 @@ export interface RulesErrors {
export const getExportByObjectIds = async (
rulesClient: RulesClient,
+ exceptionsClient: ExceptionListClient | undefined,
savedObjectsClient: AlertServices['savedObjectsClient'],
objects: Array<{ rule_id: string }>,
logger: Logger,
@@ -47,6 +50,7 @@ export const getExportByObjectIds = async (
): Promise<{
rulesNdjson: string;
exportDetails: string;
+ exceptionLists: string | null;
}> => {
const rulesAndErrors = await getRulesFromObjects(
rulesClient,
@@ -56,9 +60,19 @@ export const getExportByObjectIds = async (
isRuleRegistryEnabled
);
+ // Retrieve exceptions
+ const exceptions = rulesAndErrors.rules.flatMap((rule) => rule.exceptions_list ?? []);
+ const { exportData: exceptionLists, exportDetails: exceptionDetails } =
+ await getRuleExceptionsForExport(exceptions, exceptionsClient);
+
const rulesNdjson = transformDataToNdjson(rulesAndErrors.rules);
- const exportDetails = getExportDetailsNdjson(rulesAndErrors.rules, rulesAndErrors.missingRules);
- return { rulesNdjson, exportDetails };
+ const exportDetails = getExportDetailsNdjson(
+ rulesAndErrors.rules,
+ rulesAndErrors.missingRules,
+ exceptionDetails
+ );
+
+ return { rulesNdjson, exportDetails, exceptionLists };
};
export const getRulesFromObjects = async (
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts
index f4d50524d27b4..171233a861466 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts
@@ -20,7 +20,7 @@ describe('getExportDetailsNdjson', () => {
const details = getExportDetailsNdjson([rule]);
const reParsed = JSON.parse(details);
expect(reParsed).toEqual({
- exported_count: 1,
+ exported_rules_count: 1,
missing_rules: [],
missing_rules_count: 0,
});
@@ -31,7 +31,7 @@ describe('getExportDetailsNdjson', () => {
const details = getExportDetailsNdjson([], [missingRule]);
const reParsed = JSON.parse(details);
expect(reParsed).toEqual({
- exported_count: 0,
+ exported_rules_count: 0,
missing_rules: [{ rule_id: 'rule-1' }],
missing_rules_count: 1,
});
@@ -49,7 +49,7 @@ describe('getExportDetailsNdjson', () => {
const details = getExportDetailsNdjson([rule1, rule2], [missingRule1, missingRule2]);
const reParsed = JSON.parse(details);
expect(reParsed).toEqual({
- exported_count: 2,
+ exported_rules_count: 2,
missing_rules: [missingRule1, missingRule2],
missing_rules_count: 2,
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts
index 7f9ec77e9df79..429bf4f2926bf 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts
@@ -9,12 +9,14 @@ import { RulesSchema } from '../../../../common/detection_engine/schemas/respons
export const getExportDetailsNdjson = (
rules: Array>,
- missingRules: Array<{ rule_id: string }> = []
+ missingRules: Array<{ rule_id: string }> = [],
+ extraMeta: Record = {}
): string => {
const stringified = JSON.stringify({
- exported_count: rules.length,
+ exported_rules_count: rules.length,
missing_rules: missingRules,
missing_rules_count: missingRules.length,
+ ...extraMeta,
});
return `${stringified}\n`;
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.test.ts
new file mode 100644
index 0000000000000..dd7e59c74601c
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.test.ts
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
+
+import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock';
+import {
+ getRuleExceptionsForExport,
+ getExportableExceptions,
+ getDefaultExportDetails,
+} from './get_export_rule_exceptions';
+import {
+ getListArrayMock,
+ getListMock,
+} from '../../../../common/detection_engine/schemas/types/lists.mock';
+
+describe('get_export_rule_exceptions', () => {
+ describe('getRuleExceptionsForExport', () => {
+ test('it returns empty exceptions array if no rules have exceptions associated', async () => {
+ const { exportData, exportDetails } = await getRuleExceptionsForExport(
+ [],
+ getExceptionListClientMock()
+ );
+
+ expect(exportData).toEqual('');
+ expect(exportDetails).toEqual(getDefaultExportDetails());
+ });
+
+ test('it returns stringified exceptions ready for export', async () => {
+ const { exportData } = await getRuleExceptionsForExport(
+ [getListMock()],
+ getExceptionListClientMock()
+ );
+
+ expect(exportData).toEqual('exportString');
+ });
+
+ test('it does not return a global endpoint list', async () => {
+ const { exportData } = await getRuleExceptionsForExport(
+ [
+ {
+ id: ENDPOINT_LIST_ID,
+ list_id: ENDPOINT_LIST_ID,
+ namespace_type: 'agnostic',
+ type: 'endpoint',
+ },
+ ],
+ getExceptionListClientMock()
+ );
+
+ expect(exportData).toEqual('');
+ });
+ });
+
+ describe('getExportableExceptions', () => {
+ test('it returns stringified exception lists and items', async () => {
+ // This rule has 2 exception lists tied to it
+ const { exportData } = await getExportableExceptions(
+ getListArrayMock(),
+ getExceptionListClientMock()
+ );
+
+ expect(exportData).toEqual('exportStringexportString');
+ });
+
+ test('it throws error if error occurs in getting exceptions', async () => {
+ const exceptionsClient = getExceptionListClientMock();
+ exceptionsClient.exportExceptionListAndItems = jest.fn().mockRejectedValue(new Error('oops'));
+ // This rule has 2 exception lists tied to it
+ await expect(async () => {
+ await getExportableExceptions(getListArrayMock(), exceptionsClient);
+ }).rejects.toThrowErrorMatchingInlineSnapshot(`"oops"`);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.ts
new file mode 100644
index 0000000000000..719649d35c0f0
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { chunk } from 'lodash/fp';
+import { ListArray } from '@kbn/securitysolution-io-ts-list-types';
+import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
+
+import {
+ ExceptionListClient,
+ ExportExceptionListAndItemsReturn,
+} from '../../../../../lists/server';
+
+const NON_EXPORTABLE_LIST_IDS = [ENDPOINT_LIST_ID];
+export const EXCEPTIONS_EXPORT_CHUNK_SIZE = 50;
+
+export const getRuleExceptionsForExport = async (
+ exceptions: ListArray,
+ exceptionsListClient: ExceptionListClient | undefined
+): Promise => {
+ if (exceptionsListClient != null) {
+ const exceptionsWithoutUnexportableLists = exceptions.filter(
+ ({ list_id: listId }) => !NON_EXPORTABLE_LIST_IDS.includes(listId)
+ );
+ return getExportableExceptions(exceptionsWithoutUnexportableLists, exceptionsListClient);
+ } else {
+ return { exportData: '', exportDetails: getDefaultExportDetails() };
+ }
+};
+
+export const getExportableExceptions = async (
+ exceptions: ListArray,
+ exceptionsListClient: ExceptionListClient
+): Promise => {
+ let exportString = '';
+ const exportDetails = getDefaultExportDetails();
+
+ const exceptionChunks = chunk(EXCEPTIONS_EXPORT_CHUNK_SIZE, exceptions);
+ for await (const exceptionChunk of exceptionChunks) {
+ const promises = createPromises(exceptionsListClient, exceptionChunk);
+
+ const responses = await Promise.all(promises);
+
+ for (const res of responses) {
+ if (res != null) {
+ const {
+ exportDetails: {
+ exported_exception_list_count: exportedExceptionListCount,
+ exported_exception_list_item_count: exportedExceptionListItemCount,
+ },
+ exportData,
+ } = res;
+
+ exportDetails.exported_exception_list_count =
+ exportDetails.exported_exception_list_count + exportedExceptionListCount;
+
+ exportDetails.exported_exception_list_item_count =
+ exportDetails.exported_exception_list_item_count + exportedExceptionListItemCount;
+
+ exportString = `${exportString}${exportData}`;
+ }
+ }
+ }
+
+ return {
+ exportDetails,
+ exportData: exportString,
+ };
+};
+
+/**
+ * Creates promises of the rules and returns them.
+ * @param exceptionsListClient Exception Lists client
+ * @param exceptions The rules to apply the update for
+ * @returns Promise of export ready exceptions.
+ */
+export const createPromises = (
+ exceptionsListClient: ExceptionListClient,
+ exceptions: ListArray
+): Array> => {
+ return exceptions.map>(
+ async ({ id, list_id: listId, namespace_type: namespaceType }) => {
+ return exceptionsListClient.exportExceptionListAndItems({
+ id,
+ listId,
+ namespaceType,
+ });
+ }
+ );
+};
+
+export const getDefaultExportDetails = () => ({
+ exported_exception_list_count: 0,
+ exported_exception_list_item_count: 0,
+ missing_exception_list_item_count: 0,
+ missing_exception_list_items: [],
+ missing_exception_lists: [],
+ missing_exception_lists_count: 0,
+});
diff --git a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts
index eb5abaee8cd3b..914c684fe8813 100644
--- a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts
+++ b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts
@@ -34,6 +34,18 @@ export const filterExportedCounts = (): Transform => {
);
};
+export const filterExportedRulesCounts = (): Transform => {
+ return createFilterStream(
+ (obj) => obj != null && !has('exported_rules_count', obj)
+ );
+};
+
+export const filterExceptions = (): Transform => {
+ return createFilterStream(
+ (obj) => obj != null && !has('list_id', obj)
+ );
+};
+
// Adaptation from: saved_objects/import/create_limit_stream.ts
export const createLimitStream = (limit: number): Transform => {
let counter = 0;
diff --git a/x-pack/plugins/uptime/e2e/config.ts b/x-pack/plugins/uptime/e2e/config.ts
index 70cc57247d490..c5d573afccd96 100644
--- a/x-pack/plugins/uptime/e2e/config.ts
+++ b/x-pack/plugins/uptime/e2e/config.ts
@@ -39,7 +39,7 @@ async function config({ readConfigFile }: FtrConfigProviderContext) {
'--csp.warnLegacyBrowsers=false',
// define custom kibana server args here
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
- `--elasticsearch.ignoreVersionMismatch=true`,
+ `--elasticsearch.ignoreVersionMismatch=${process.env.CI ? 'false' : 'true'}`,
`--uiSettings.overrides.theme:darkMode=true`,
`--elasticsearch.username=kibana_system`,
`--elasticsearch.password=changeme`,
diff --git a/x-pack/plugins/uptime/e2e/playwright_start.ts b/x-pack/plugins/uptime/e2e/playwright_start.ts
index aedb255b058be..5949339c1ba25 100644
--- a/x-pack/plugins/uptime/e2e/playwright_start.ts
+++ b/x-pack/plugins/uptime/e2e/playwright_start.ts
@@ -15,15 +15,10 @@ import './journeys';
export function playwrightRunTests() {
return async ({ getService }: any) => {
- try {
- const result = await playwrightStart(getService);
-
- if (result && result.uptime.status !== 'succeeded') {
- process.exit(1);
- }
- } catch (error) {
- console.error('errors: ', error);
- process.exit(1);
+ const result = await playwrightStart(getService);
+
+ if (result && result.uptime.status !== 'succeeded') {
+ throw new Error('Tests failed');
}
};
}
@@ -42,7 +37,7 @@ async function playwrightStart(getService: any) {
const res = await playwrightRun({
params: { kibanaUrl },
- playwrightOptions: { chromiumSandbox: false, timeout: 60 * 1000 },
+ playwrightOptions: { headless: true, chromiumSandbox: false, timeout: 60 * 1000 },
});
console.log('Removing esArchiver...');
diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx
index 4ccbbd37b041b..adb76d4cbc8ac 100644
--- a/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx
+++ b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { LineSeries, CurveType, Fit } from '@elastic/charts';
+import { LineSeries, CurveType, Fit, ScaleType } from '@elastic/charts';
import { LocationDurationLine } from '../../../../common/types';
import { microToMilli, microToSec } from '../../../lib/formatting';
import { MS_LABEL, SEC_LABEL } from '../translations';
@@ -27,9 +27,9 @@ export const DurationLineSeriesList = ({ monitorType, lines }: Props) => (
key={`loc-line-${name}`}
name={name}
xAccessor={0}
- xScaleType="time"
+ xScaleType={ScaleType.Time}
yAccessors={[1]}
- yScaleType="linear"
+ yScaleType={ScaleType.Linear}
fit={Fit.Linear}
tickFormat={(d) =>
monitorType === 'browser'
diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx
index aa981071b7ee2..5c4be0e6719f4 100644
--- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx
+++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx
@@ -15,6 +15,7 @@ import {
BrushEndListener,
XYChartElementEvent,
ElementClickListener,
+ ScaleType,
} from '@elastic/charts';
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -182,9 +183,9 @@ export const PingHistogramComponent: React.FC = ({
splitSeriesAccessors={['type']}
timeZone="local"
xAccessor="x"
- xScaleType="time"
+ xScaleType={ScaleType.Time}
yAccessors={['y']}
- yScaleType="linear"
+ yScaleType={ScaleType.Linear}
/>
diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx
index 789953258750b..26f9e28101ea4 100644
--- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx
+++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx
@@ -51,7 +51,7 @@ export function ActionMenuContent(): React.ReactElement {
allSeries: [
{
dataType: 'synthetics',
- seriesType: 'area_stacked',
+ seriesType: 'area',
selectedMetricField: 'monitor.duration.us',
time: { from: dateRangeStart, to: dateRangeEnd },
breakdown: monitorId ? 'observer.geo.name' : 'monitor.type',
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts
index 640db94028bc6..e457453a38f19 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts
+++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts
@@ -9,9 +9,9 @@ import { BrowserFields, ConfigKeys } from '../types';
import {
Formatter,
commonFormatters,
+ objectToJsonFormatter,
arrayToJsonFormatter,
stringToJsonFormatter,
- objectToJsonFormatter,
} from '../common/formatters';
import {
tlsValueToYamlFormatter,
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts
index 34b937b80dad0..2c675b9f28804 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts
+++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts
@@ -14,7 +14,6 @@ import {
} from '../common/normalizers';
import { defaultBrowserSimpleFields, defaultBrowserAdvancedFields } from '../contexts';
-import { tlsJsonToObjectNormalizer, tlsStringToObjectNormalizer } from '../tls/normalizers';
export type BrowserNormalizerMap = Record;
@@ -42,33 +41,22 @@ export const browserNormalizers: BrowserNormalizerMap = {
[ConfigKeys.PARAMS]: getBrowserNormalizer(ConfigKeys.PARAMS),
[ConfigKeys.SCREENSHOTS]: getBrowserNormalizer(ConfigKeys.SCREENSHOTS),
[ConfigKeys.SYNTHETICS_ARGS]: getBrowserJsonToJavascriptNormalizer(ConfigKeys.SYNTHETICS_ARGS),
- [ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: (fields) =>
- tlsJsonToObjectNormalizer(
- fields?.[ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]?.value,
- ConfigKeys.TLS_CERTIFICATE_AUTHORITIES
- ),
- [ConfigKeys.ZIP_URL_TLS_CERTIFICATE]: (fields) =>
- tlsJsonToObjectNormalizer(
- fields?.[ConfigKeys.ZIP_URL_TLS_CERTIFICATE]?.value,
- ConfigKeys.TLS_CERTIFICATE
- ),
- [ConfigKeys.ZIP_URL_TLS_KEY]: (fields) =>
- tlsJsonToObjectNormalizer(fields?.[ConfigKeys.ZIP_URL_TLS_KEY]?.value, ConfigKeys.TLS_KEY),
- [ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]: (fields) =>
- tlsStringToObjectNormalizer(
- fields?.[ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]?.value,
- ConfigKeys.TLS_KEY_PASSPHRASE
- ),
- [ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]: (fields) =>
- tlsStringToObjectNormalizer(
- fields?.[ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]?.value,
- ConfigKeys.TLS_VERIFICATION_MODE
- ),
- [ConfigKeys.ZIP_URL_TLS_VERSION]: (fields) =>
- tlsJsonToObjectNormalizer(
- fields?.[ConfigKeys.ZIP_URL_TLS_VERSION]?.value,
- ConfigKeys.TLS_VERSION
- ),
+ [ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: getBrowserJsonToJavascriptNormalizer(
+ ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES
+ ),
+ [ConfigKeys.ZIP_URL_TLS_CERTIFICATE]: getBrowserJsonToJavascriptNormalizer(
+ ConfigKeys.ZIP_URL_TLS_CERTIFICATE
+ ),
+ [ConfigKeys.ZIP_URL_TLS_KEY]: getBrowserJsonToJavascriptNormalizer(ConfigKeys.ZIP_URL_TLS_KEY),
+ [ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]: getBrowserNormalizer(
+ ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE
+ ),
+ [ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]: getBrowserNormalizer(
+ ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE
+ ),
+ [ConfigKeys.ZIP_URL_TLS_VERSION]: getBrowserJsonToJavascriptNormalizer(
+ ConfigKeys.ZIP_URL_TLS_VERSION
+ ),
[ConfigKeys.JOURNEY_FILTERS_MATCH]: getBrowserJsonToJavascriptNormalizer(
ConfigKeys.JOURNEY_FILTERS_MATCH
),
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.test.tsx
new file mode 100644
index 0000000000000..b19ca47ab76a5
--- /dev/null
+++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.test.tsx
@@ -0,0 +1,125 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { fireEvent, waitFor } from '@testing-library/react';
+import { render } from '../../../lib/helper/rtl_helpers';
+import { ScriptRecorderFields } from './script_recorder_fields';
+import { PolicyConfigContextProvider } from '../contexts';
+
+jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
+ ...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'),
+ htmlIdGenerator: () => () => `id-${Math.random()}`,
+}));
+
+const onChange = jest.fn();
+
+describe('', () => {
+ let file: File;
+ const testScript = 'step(() => {})';
+ const WrappedComponent = ({
+ isEditable = true,
+ script = '',
+ fileName = '',
+ }: {
+ isEditable?: boolean;
+ script?: string;
+ fileName?: string;
+ }) => {
+ return (
+
+
+
+ );
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ file = new File([testScript], 'samplescript.js', { type: 'text/javascript' });
+ });
+
+ it('renders ScriptRecorderFields', () => {
+ const { getByText, queryByText } = render();
+
+ const downloadLink = getByText('Download the Elastic Synthetics Recorder');
+
+ expect(downloadLink).toBeInTheDocument();
+ expect(downloadLink).toHaveAttribute('target', '_blank');
+
+ expect(queryByText('Show script')).not.toBeInTheDocument();
+ expect(queryByText('Remove script')).not.toBeInTheDocument();
+ });
+
+ it('handles uploading files', async () => {
+ const { getByTestId } = render();
+
+ const uploader = getByTestId('syntheticsFleetScriptRecorderUploader');
+
+ fireEvent.change(uploader, {
+ target: { files: [file] },
+ });
+
+ await waitFor(() => {
+ expect(onChange).toBeCalledWith({ scriptText: testScript, fileName: 'samplescript.js' });
+ });
+ });
+
+ it('shows user errors for invalid file types', async () => {
+ const { getByTestId, getByText } = render();
+ file = new File(['journey(() => {})'], 'samplescript.js', { type: 'text/javascript' });
+
+ let uploader = getByTestId('syntheticsFleetScriptRecorderUploader') as HTMLInputElement;
+
+ fireEvent.change(uploader, {
+ target: { files: [file] },
+ });
+
+ uploader = getByTestId('syntheticsFleetScriptRecorderUploader') as HTMLInputElement;
+
+ await waitFor(() => {
+ expect(onChange).not.toBeCalled();
+ expect(
+ getByText(
+ 'Error uploading file. Please upload a .js file generated by the Elastic Synthetics Recorder in inline script format.'
+ )
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('shows show script button when script is available', () => {
+ const { getByText, queryByText } = render();
+
+ const showScriptBtn = getByText('Show script');
+
+ expect(queryByText(testScript)).not.toBeInTheDocument();
+
+ fireEvent.click(showScriptBtn);
+
+ expect(getByText(testScript)).toBeInTheDocument();
+ });
+
+ it('shows show remove script button when script is available and isEditable is true', async () => {
+ const { getByText, getByTestId } = render(
+
+ );
+
+ const showScriptBtn = getByText('Show script');
+ fireEvent.click(showScriptBtn);
+
+ expect(getByText(testScript)).toBeInTheDocument();
+
+ fireEvent.click(getByTestId('euiFlyoutCloseButton'));
+
+ const removeScriptBtn = getByText('Remove script');
+
+ fireEvent.click(removeScriptBtn);
+
+ await waitFor(() => {
+ expect(onChange).toBeCalledWith({ scriptText: '', fileName: '' });
+ });
+ });
+});
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.tsx
new file mode 100644
index 0000000000000..9b99b4094e63b
--- /dev/null
+++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.tsx
@@ -0,0 +1,134 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState, useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiLink,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiFormRow,
+ EuiCodeBlock,
+ EuiTitle,
+ EuiButton,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+import { usePolicyConfigContext } from '../contexts/policy_config_context';
+import { Uploader } from './uploader';
+
+interface Props {
+ onChange: ({ scriptText, fileName }: { scriptText: string; fileName: string }) => void;
+ script: string;
+ fileName?: string;
+}
+
+export function ScriptRecorderFields({ onChange, script, fileName }: Props) {
+ const [showScript, setShowScript] = useState(false);
+ const { isEditable } = usePolicyConfigContext();
+
+ const handleUpload = useCallback(
+ ({ scriptText, fileName: fileNameT }: { scriptText: string; fileName: string }) => {
+ onChange({ scriptText, fileName: fileNameT });
+ },
+ [onChange]
+ );
+
+ return (
+ <>
+
+
+
+
+
+ {isEditable && script ? (
+
+
+ {fileName}
+
+
+ ) : (
+
+ )}
+ {script && (
+ <>
+
+
+
+ setShowScript(true)}
+ iconType="editorCodeBlock"
+ iconSide="right"
+ >
+
+
+
+
+ {isEditable && (
+ onChange({ scriptText: '', fileName: '' })}
+ iconType="trash"
+ iconSide="right"
+ color="danger"
+ >
+
+
+ )}
+
+
+ >
+ )}
+ {showScript && (
+ setShowScript(false)}
+ aria-labelledby="syntheticsBrowserScriptBlockHeader"
+ closeButtonAriaLabel={CLOSE_BUTTON_LABEL}
+ >
+
+
+
+
+
+
+
+ {script}
+
+
+
+ )}
+ >
+ );
+}
+
+const PLACEHOLDER_FILE_NAME = i18n.translate(
+ 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.mockFileName',
+ {
+ defaultMessage: 'test_script.js',
+ }
+);
+
+const CLOSE_BUTTON_LABEL = i18n.translate(
+ 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.closeButtonLabel',
+ {
+ defaultMessage: 'Close script flyout',
+ }
+);
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx
index 775778296fba8..50ad14aa98287 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx
@@ -24,7 +24,17 @@ export const BrowserSimpleFields = memo(({ validate }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
};
const onChangeSourceField = useCallback(
- ({ zipUrl, folder, username, password, inlineScript, params, proxyUrl }) => {
+ ({
+ zipUrl,
+ folder,
+ username,
+ password,
+ inlineScript,
+ params,
+ proxyUrl,
+ isGeneratedScript,
+ fileName,
+ }) => {
setFields((prevFields) => ({
...prevFields,
[ConfigKeys.SOURCE_ZIP_URL]: zipUrl,
@@ -34,6 +44,13 @@ export const BrowserSimpleFields = memo(({ validate }) => {
[ConfigKeys.SOURCE_ZIP_PASSWORD]: password,
[ConfigKeys.SOURCE_INLINE]: inlineScript,
[ConfigKeys.PARAMS]: params,
+ [ConfigKeys.METADATA]: {
+ ...prevFields[ConfigKeys.METADATA],
+ script_source: {
+ is_generated_script: isGeneratedScript,
+ file_name: fileName,
+ },
+ },
}));
},
[setFields]
@@ -87,6 +104,9 @@ export const BrowserSimpleFields = memo(({ validate }) => {
password: defaultValues[ConfigKeys.SOURCE_ZIP_PASSWORD],
inlineScript: defaultValues[ConfigKeys.SOURCE_INLINE],
params: defaultValues[ConfigKeys.PARAMS],
+ isGeneratedScript:
+ defaultValues[ConfigKeys.METADATA].script_source?.is_generated_script,
+ fileName: defaultValues[ConfigKeys.METADATA].script_source?.file_name,
}),
[defaultValues]
)}
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx
index 6f2a7c99ad0d5..e4b03e53432dd 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx
@@ -5,23 +5,28 @@
* 2.0.
*/
import React, { useEffect, useState } from 'react';
+import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-
import {
EuiTabbedContent,
EuiFormRow,
EuiFieldText,
EuiFieldPassword,
EuiSpacer,
+ EuiBetaBadge,
+ EuiFlexGroup,
+ EuiFlexItem,
} from '@elastic/eui';
import { OptionalLabel } from '../optional_label';
import { CodeEditor } from '../code_editor';
+import { ScriptRecorderFields } from './script_recorder_fields';
import { ZipUrlTLSFields } from './zip_url_tls_fields';
import { MonacoEditorLangId } from '../types';
enum SourceType {
INLINE = 'syntheticsBrowserInlineConfig',
+ SCRIPT_RECORDER = 'syntheticsBrowserScriptRecorderConfig',
ZIP = 'syntheticsBrowserZipURLConfig',
}
@@ -33,6 +38,8 @@ interface SourceConfig {
password: string;
inlineScript: string;
params: string;
+ isGeneratedScript?: boolean;
+ fileName?: string;
}
interface Props {
@@ -48,12 +55,22 @@ export const defaultValues = {
password: '',
inlineScript: '',
params: '',
+ isGeneratedScript: false,
+ fileName: '',
+};
+
+const getDefaultTab = (defaultConfig: SourceConfig) => {
+ if (defaultConfig.inlineScript && defaultConfig.isGeneratedScript) {
+ return SourceType.SCRIPT_RECORDER;
+ } else if (defaultConfig.inlineScript) {
+ return SourceType.INLINE;
+ }
+
+ return SourceType.ZIP;
};
export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props) => {
- const [sourceType, setSourceType] = useState(
- defaultConfig.inlineScript ? SourceType.INLINE : SourceType.ZIP
- );
+ const [sourceType, setSourceType] = useState(getDefaultTab(defaultConfig));
const [config, setConfig] = useState(defaultConfig);
useEffect(() => {
@@ -264,6 +281,52 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props)
),
},
+ {
+ id: 'syntheticsBrowserScriptRecorderConfig',
+ name: (
+
+
+
+
+
+
+
+
+ ),
+ 'data-test-subj': 'syntheticsSourceTab__scriptRecorder',
+ content: (
+
+ setConfig((prevConfig) => ({
+ ...prevConfig,
+ inlineScript: scriptText,
+ isGeneratedScript: true,
+ fileName,
+ }))
+ }
+ script={config.inlineScript}
+ fileName={config.fileName}
+ />
+ ),
+ },
];
return (
@@ -272,11 +335,17 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props)
initialSelectedTab={tabs.find((tab) => tab.id === sourceType)}
autoFocus="selected"
onTabClick={(tab) => {
- setSourceType(tab.id as SourceType);
if (tab.id !== sourceType) {
setConfig(defaultValues);
}
+ setSourceType(tab.id as SourceType);
}}
/>
);
};
+
+const StyledBetaBadgeWrapper = styled(EuiFlexItem)`
+ .euiToolTipAnchor {
+ display: flex;
+ }
+`;
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/uploader.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/uploader.tsx
new file mode 100644
index 0000000000000..f17f11ebdccb6
--- /dev/null
+++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/uploader.tsx
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState, useRef } from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { EuiFormRow, EuiFilePicker } from '@elastic/eui';
+
+interface Props {
+ onUpload: ({ scriptText, fileName }: { scriptText: string; fileName: string }) => void;
+}
+
+export function Uploader({ onUpload }: Props) {
+ const fileReader = useRef(null);
+ const [error, setError] = useState(null);
+ const filePickerRef = useRef(null);
+
+ const handleFileRead = (fileName: string) => {
+ const content = fileReader?.current?.result as string;
+
+ if (content?.trim().slice(0, 4) !== 'step') {
+ setError(PARSING_ERROR);
+ filePickerRef.current?.removeFiles();
+ return;
+ }
+
+ onUpload({ scriptText: content, fileName });
+ setError(null);
+ };
+
+ const handleFileChosen = (files: FileList | null) => {
+ if (!files || !files.length) {
+ onUpload({ scriptText: '', fileName: '' });
+ return;
+ }
+ if (files.length && files[0].type !== 'text/javascript') {
+ setError(INVALID_FILE_ERROR);
+ filePickerRef.current?.removeFiles();
+ return;
+ }
+ fileReader.current = new FileReader();
+ fileReader.current.onloadend = () => handleFileRead(files[0].name);
+ fileReader.current.readAsText(files[0]);
+ };
+
+ return (
+
+
+
+ );
+}
+
+const TESTING_SCRIPT_LABEL = i18n.translate(
+ 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.fieldLabel',
+ {
+ defaultMessage: 'Testing script',
+ }
+);
+
+const PROMPT_TEXT = i18n.translate(
+ 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.label',
+ {
+ defaultMessage: 'Select recorder-generated .js file',
+ }
+);
+
+const INVALID_FILE_ERROR = i18n.translate(
+ 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.invalidFileError',
+ {
+ defaultMessage:
+ 'Invalid file type. Please upload a .js file generated by the Elastic Synthetics Recorder.',
+ }
+);
+
+const PARSING_ERROR = i18n.translate(
+ 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.parsingError',
+ {
+ defaultMessage:
+ 'Error uploading file. Please upload a .js file generated by the Elastic Synthetics Recorder in inline script format.',
+ }
+);
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/zip_url_tls_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/zip_url_tls_fields.tsx
index bd5d2f3e5d4a2..ed1ad9a8ce65c 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/browser/zip_url_tls_fields.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/zip_url_tls_fields.tsx
@@ -12,7 +12,11 @@ import { EuiSwitch, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { TLSOptions, TLSConfig } from '../common/tls_options';
-import { useBrowserSimpleFieldsContext, usePolicyConfigContext } from '../contexts';
+import {
+ useBrowserSimpleFieldsContext,
+ usePolicyConfigContext,
+ defaultTLSFields,
+} from '../contexts';
import { ConfigKeys } from '../types';
@@ -67,12 +71,23 @@ export const ZipUrlTLSFields = () => {
{isZipUrlTLSEnabled ? (
>;
@@ -24,6 +23,10 @@ interface IBrowserSimpleFieldsContextProvider {
export const initialValues: IBrowserSimpleFields = {
...commonDefaultValues,
[ConfigKeys.METADATA]: {
+ script_source: {
+ is_generated_script: false,
+ file_name: '',
+ },
is_zip_url_tls_enabled: false,
},
[ConfigKeys.MONITOR_TYPE]: DataStream.BROWSER,
@@ -34,13 +37,12 @@ export const initialValues: IBrowserSimpleFields = {
[ConfigKeys.SOURCE_ZIP_PROXY_URL]: '',
[ConfigKeys.SOURCE_INLINE]: '',
[ConfigKeys.PARAMS]: '',
- [ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]:
- tlsDefaultValues[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES],
- [ConfigKeys.ZIP_URL_TLS_CERTIFICATE]: tlsDefaultValues[ConfigKeys.TLS_CERTIFICATE],
- [ConfigKeys.ZIP_URL_TLS_KEY]: tlsDefaultValues[ConfigKeys.TLS_KEY],
- [ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]: tlsDefaultValues[ConfigKeys.TLS_KEY_PASSPHRASE],
- [ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]: tlsDefaultValues[ConfigKeys.TLS_VERIFICATION_MODE],
- [ConfigKeys.ZIP_URL_TLS_VERSION]: tlsDefaultValues[ConfigKeys.TLS_VERSION],
+ [ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: undefined,
+ [ConfigKeys.ZIP_URL_TLS_CERTIFICATE]: undefined,
+ [ConfigKeys.ZIP_URL_TLS_KEY]: undefined,
+ [ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]: undefined,
+ [ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]: undefined,
+ [ConfigKeys.ZIP_URL_TLS_VERSION]: undefined,
};
const defaultContext: IBrowserSimpleFieldsContext = {
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts
index 3d08dec46a4be..df2e9cfa6d4ea 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts
+++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts
@@ -7,7 +7,7 @@
export {
PolicyConfigContext,
PolicyConfigContextProvider,
- initialValue as defaultMonitorType,
+ initialValue as defaultPolicyConfig,
usePolicyConfigContext,
} from './policy_config_context';
export {
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx
index 535a415c9a43d..69c0e1d7ba4fe 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx
@@ -18,6 +18,7 @@ interface IPolicyConfigContext {
isZipUrlTLSEnabled?: boolean;
defaultIsTLSEnabled?: boolean;
defaultIsZipUrlTLSEnabled?: boolean;
+ isEditable?: boolean;
}
interface IPolicyConfigContextProvider {
@@ -25,6 +26,7 @@ interface IPolicyConfigContextProvider {
defaultMonitorType?: DataStream;
defaultIsTLSEnabled?: boolean;
defaultIsZipUrlTLSEnabled?: boolean;
+ isEditable?: boolean;
}
export const initialValue = DataStream.HTTP;
@@ -45,6 +47,7 @@ const defaultContext: IPolicyConfigContext = {
defaultMonitorType: initialValue, // immutable,
defaultIsTLSEnabled: false,
defaultIsZipUrlTLSEnabled: false,
+ isEditable: false,
};
export const PolicyConfigContext = createContext(defaultContext);
@@ -54,6 +57,7 @@ export const PolicyConfigContextProvider = ({
defaultMonitorType = initialValue,
defaultIsTLSEnabled = false,
defaultIsZipUrlTLSEnabled = false,
+ isEditable = false,
}: IPolicyConfigContextProvider) => {
const [monitorType, setMonitorType] = useState(defaultMonitorType);
const [isTLSEnabled, setIsTLSEnabled] = useState(defaultIsTLSEnabled);
@@ -70,6 +74,7 @@ export const PolicyConfigContextProvider = ({
setIsZipUrlTLSEnabled,
defaultIsTLSEnabled,
defaultIsZipUrlTLSEnabled,
+ isEditable,
};
}, [
monitorType,
@@ -78,6 +83,7 @@ export const PolicyConfigContextProvider = ({
isZipUrlTLSEnabled,
defaultIsTLSEnabled,
defaultIsZipUrlTLSEnabled,
+ isEditable,
]);
return ;
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx
index 62c6f5598adb4..03bba0f8d2e54 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx
@@ -54,21 +54,17 @@ const defaultTCPConfig = defaultConfig[DataStream.TCP];
describe.skip('', () => {
const WrappedComponent = ({
validate = defaultValidation,
- typeEditable = false,
+ isEditable = false,
dataStreams = [DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER],
}) => {
return (
-
+
-
+
@@ -80,7 +76,7 @@ describe.skip('', () => {
it('renders CustomFields', async () => {
const { getByText, getByLabelText, queryByLabelText } = render();
- const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement;
+ const monitorType = getByLabelText('Monitor Type') as HTMLInputElement;
const url = getByLabelText('URL') as HTMLInputElement;
const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement;
const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement;
@@ -88,7 +84,7 @@ describe.skip('', () => {
const apmServiceName = getByLabelText('APM service name') as HTMLInputElement;
const maxRedirects = getByLabelText('Max redirects') as HTMLInputElement;
const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement;
- expect(monitorType).not.toBeInTheDocument();
+ expect(monitorType).toBeInTheDocument();
expect(url).toBeInTheDocument();
expect(url.value).toEqual(defaultHTTPConfig[ConfigKeys.URLS]);
expect(proxyUrl).toBeInTheDocument();
@@ -117,6 +113,13 @@ describe.skip('', () => {
});
});
+ it('does not show monitor type dropdown when isEditable is true', async () => {
+ const { queryByLabelText } = render();
+ const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement;
+
+ expect(monitorType).not.toBeInTheDocument();
+ });
+
it('shows SSL fields when Enable SSL Fields is checked', async () => {
const { findByLabelText, queryByLabelText } = render();
const enableSSL = queryByLabelText('Enable TLS configuration') as HTMLInputElement;
@@ -180,7 +183,7 @@ describe.skip('', () => {
it('handles switching monitor type', () => {
const { getByText, getByLabelText, queryByLabelText, getAllByLabelText } = render(
-
+
);
const monitorType = getByLabelText('Monitor Type') as HTMLInputElement;
expect(monitorType).toBeInTheDocument();
@@ -244,7 +247,7 @@ describe.skip('', () => {
});
it('shows resolve hostnames locally field when proxy url is filled for tcp monitors', () => {
- const { getByLabelText, queryByLabelText } = render();
+ const { getByLabelText, queryByLabelText } = render();
const monitorType = getByLabelText('Monitor Type') as HTMLInputElement;
fireEvent.change(monitorType, { target: { value: DataStream.TCP } });
@@ -302,10 +305,7 @@ describe.skip('', () => {
it('does not show monitor options that are not contained in datastreams', async () => {
const { getByText, queryByText, queryByLabelText } = render(
-
+
);
const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement;
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx
index 7323464f3e9dd..98eac21a42076 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx
@@ -30,13 +30,13 @@ import { BrowserSimpleFields } from './browser/simple_fields';
import { BrowserAdvancedFields } from './browser/advanced_fields';
interface Props {
- typeEditable?: boolean;
validate: Validation;
dataStreams?: DataStream[];
}
-export const CustomFields = memo(({ typeEditable = false, validate, dataStreams = [] }) => {
- const { monitorType, setMonitorType, isTLSEnabled, setIsTLSEnabled } = usePolicyConfigContext();
+export const CustomFields = memo(({ validate, dataStreams = [] }) => {
+ const { monitorType, setMonitorType, isTLSEnabled, setIsTLSEnabled, isEditable } =
+ usePolicyConfigContext();
const isHTTP = monitorType === DataStream.HTTP;
const isTCP = monitorType === DataStream.TCP;
@@ -88,7 +88,7 @@ export const CustomFields = memo(({ typeEditable = false, validate, dataS
>
- {typeEditable && (
+ {!isEditable && (
{
+ const { isTLSEnabled, isZipUrlTLSEnabled } = usePolicyConfigContext();
+ const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext();
+ const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext();
+ const { fields: icmpSimpleFields } = useICMPSimpleFieldsContext();
+ const { fields: browserSimpleFields } = useBrowserSimpleFieldsContext();
+ const { fields: httpAdvancedFields } = useHTTPAdvancedFieldsContext();
+ const { fields: tcpAdvancedFields } = useTCPAdvancedFieldsContext();
+ const { fields: browserAdvancedFields } = useBrowserAdvancedFieldsContext();
+ const { fields: tlsFields } = useTLSFieldsContext();
+
+ const metadata = useMemo(
+ () => ({
+ is_tls_enabled: isTLSEnabled,
+ is_zip_url_tls_enabled: isZipUrlTLSEnabled,
+ }),
+ [isTLSEnabled, isZipUrlTLSEnabled]
+ );
+
+ const policyConfig: PolicyConfig = useMemo(
+ () => ({
+ [DataStream.HTTP]: {
+ ...httpSimpleFields,
+ ...httpAdvancedFields,
+ ...tlsFields,
+ [ConfigKeys.METADATA]: {
+ ...httpSimpleFields[ConfigKeys.METADATA],
+ ...metadata,
+ },
+ [ConfigKeys.NAME]: name,
+ } as HTTPFields,
+ [DataStream.TCP]: {
+ ...tcpSimpleFields,
+ ...tcpAdvancedFields,
+ ...tlsFields,
+ [ConfigKeys.METADATA]: {
+ ...tcpSimpleFields[ConfigKeys.METADATA],
+ ...metadata,
+ },
+ [ConfigKeys.NAME]: name,
+ } as TCPFields,
+ [DataStream.ICMP]: {
+ ...icmpSimpleFields,
+ [ConfigKeys.NAME]: name,
+ } as ICMPFields,
+ [DataStream.BROWSER]: {
+ ...browserSimpleFields,
+ ...browserAdvancedFields,
+ [ConfigKeys.METADATA]: {
+ ...browserSimpleFields[ConfigKeys.METADATA],
+ ...metadata,
+ },
+ [ConfigKeys.NAME]: name,
+ } as BrowserFields,
+ }),
+ [
+ metadata,
+ httpSimpleFields,
+ httpAdvancedFields,
+ tcpSimpleFields,
+ tcpAdvancedFields,
+ icmpSimpleFields,
+ browserSimpleFields,
+ browserAdvancedFields,
+ tlsFields,
+ name,
+ ]
+ );
+
+ return policyConfig;
+};
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.test.tsx
similarity index 99%
rename from x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx
rename to x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.test.tsx
index 3bffab33adb1e..dc46a4b57bcd8 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.test.tsx
@@ -4,11 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
-import { useUpdatePolicy } from './use_update_policy';
import { renderHook } from '@testing-library/react-hooks';
-import { NewPackagePolicy } from '../../../../fleet/public';
-import { validate } from './validation';
+import { useUpdatePolicy } from './use_update_policy';
+import { NewPackagePolicy } from '../../../../../fleet/public';
+import { validate } from '../validation';
import {
ConfigKeys,
DataStream,
@@ -20,8 +19,8 @@ import {
ITLSFields,
HTTPFields,
BrowserFields,
-} from './types';
-import { defaultConfig } from './synthetics_policy_create_extension';
+} from '../types';
+import { defaultConfig } from '../synthetics_policy_create_extension';
describe('useBarChartsHooks', () => {
const newPolicy: NewPackagePolicy = {
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts b/x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.ts
similarity index 95%
rename from x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts
rename to x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.ts
index 145a86c6bd50d..17ded6385da4f 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts
+++ b/x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
import { useEffect, useRef, useState } from 'react';
-import { NewPackagePolicy } from '../../../../fleet/public';
-import { ConfigKeys, DataStream, Validation, ICustomFields } from './types';
-import { formatters } from './helpers/formatters';
+import { NewPackagePolicy } from '../../../../../fleet/public';
+import { ConfigKeys, DataStream, Validation, ICustomFields } from '../types';
+import { formatters } from '../helpers/formatters';
interface Props {
monitorType: DataStream;
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx
index 3db7ac424e651..4fa101a329cd0 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx
@@ -8,36 +8,21 @@
import React, { memo, useEffect, useMemo } from 'react';
import { PackagePolicyCreateExtensionComponentProps } from '../../../../fleet/public';
import { useTrackPageview } from '../../../../observability/public';
-import {
- PolicyConfig,
- DataStream,
- ConfigKeys,
- HTTPFields,
- TCPFields,
- ICMPFields,
- BrowserFields,
-} from './types';
+import { PolicyConfig, DataStream } from './types';
import {
usePolicyConfigContext,
- useTCPSimpleFieldsContext,
- useTCPAdvancedFieldsContext,
- useICMPSimpleFieldsContext,
- useHTTPSimpleFieldsContext,
- useHTTPAdvancedFieldsContext,
- useTLSFieldsContext,
- useBrowserSimpleFieldsContext,
- useBrowserAdvancedFieldsContext,
- defaultHTTPAdvancedFields,
defaultHTTPSimpleFields,
- defaultICMPSimpleFields,
+ defaultHTTPAdvancedFields,
defaultTCPSimpleFields,
defaultTCPAdvancedFields,
+ defaultICMPSimpleFields,
defaultBrowserSimpleFields,
defaultBrowserAdvancedFields,
defaultTLSFields,
} from './contexts';
import { CustomFields } from './custom_fields';
-import { useUpdatePolicy } from './use_update_policy';
+import { useUpdatePolicy } from './hooks/use_update_policy';
+import { usePolicy } from './hooks/use_policy';
import { validate } from './validation';
export const defaultConfig: PolicyConfig = {
@@ -65,55 +50,12 @@ export const defaultConfig: PolicyConfig = {
*/
export const SyntheticsPolicyCreateExtension = memo(
({ newPolicy, onChange }) => {
- const { monitorType, isTLSEnabled, isZipUrlTLSEnabled } = usePolicyConfigContext();
- const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext();
- const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext();
- const { fields: icmpSimpleFields } = useICMPSimpleFieldsContext();
- const { fields: browserSimpleFields } = useBrowserSimpleFieldsContext();
- const { fields: httpAdvancedFields } = useHTTPAdvancedFieldsContext();
- const { fields: tcpAdvancedFields } = useTCPAdvancedFieldsContext();
- const { fields: browserAdvancedFields } = useBrowserAdvancedFieldsContext();
- const { fields: tlsFields } = useTLSFieldsContext();
-
- const metaData = useMemo(
- () => ({
- is_tls_enabled: isTLSEnabled,
- is_zip_url_tls_enabled: isZipUrlTLSEnabled,
- }),
- [isTLSEnabled, isZipUrlTLSEnabled]
- );
-
- const policyConfig: PolicyConfig = {
- [DataStream.HTTP]: {
- ...httpSimpleFields,
- ...httpAdvancedFields,
- ...tlsFields,
- [ConfigKeys.METADATA]: metaData,
- [ConfigKeys.NAME]: newPolicy.name,
- } as HTTPFields,
- [DataStream.TCP]: {
- ...tcpSimpleFields,
- ...tcpAdvancedFields,
- ...tlsFields,
- [ConfigKeys.METADATA]: metaData,
- [ConfigKeys.NAME]: newPolicy.name,
- } as TCPFields,
- [DataStream.ICMP]: {
- ...icmpSimpleFields,
- [ConfigKeys.NAME]: newPolicy.name,
- } as ICMPFields,
- [DataStream.BROWSER]: {
- ...browserSimpleFields,
- ...browserAdvancedFields,
- ...tlsFields,
- [ConfigKeys.METADATA]: metaData,
- [ConfigKeys.NAME]: newPolicy.name,
- } as BrowserFields,
- };
-
useTrackPageview({ app: 'fleet', path: 'syntheticsCreate' });
useTrackPageview({ app: 'fleet', path: 'syntheticsCreate', delay: 15000 });
+ const { monitorType } = usePolicyConfigContext();
+ const policyConfig: PolicyConfig = usePolicy(newPolicy.name);
+
const dataStreams: DataStream[] = useMemo(() => {
return newPolicy.inputs.map((input) => {
return input.type.replace(/synthetics\//g, '') as DataStream;
@@ -143,7 +85,7 @@ export const SyntheticsPolicyCreateExtension = memo;
+ return ;
}
);
SyntheticsPolicyCreateExtension.displayName = 'SyntheticsPolicyCreateExtension';
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx
index 8e441d4eed6e3..1e01f43439a31 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx
@@ -5,32 +5,14 @@
* 2.0.
*/
-import React, { memo, useMemo } from 'react';
+import React, { memo } from 'react';
import { PackagePolicyEditExtensionComponentProps } from '../../../../fleet/public';
import { useTrackPageview } from '../../../../observability/public';
-import {
- usePolicyConfigContext,
- useTCPSimpleFieldsContext,
- useTCPAdvancedFieldsContext,
- useICMPSimpleFieldsContext,
- useHTTPSimpleFieldsContext,
- useHTTPAdvancedFieldsContext,
- useTLSFieldsContext,
- useBrowserSimpleFieldsContext,
- useBrowserAdvancedFieldsContext,
-} from './contexts';
-import {
- ICustomFields,
- DataStream,
- HTTPFields,
- TCPFields,
- ICMPFields,
- BrowserFields,
- ConfigKeys,
- PolicyConfig,
-} from './types';
+import { usePolicyConfigContext } from './contexts';
+import { ICustomFields, PolicyConfig } from './types';
import { CustomFields } from './custom_fields';
-import { useUpdatePolicy } from './use_update_policy';
+import { useUpdatePolicy } from './hooks/use_update_policy';
+import { usePolicy } from './hooks/use_policy';
import { validate } from './validation';
interface SyntheticsPolicyEditExtensionProps {
@@ -47,50 +29,9 @@ export const SyntheticsPolicyEditExtension = memo {
useTrackPageview({ app: 'fleet', path: 'syntheticsEdit' });
useTrackPageview({ app: 'fleet', path: 'syntheticsEdit', delay: 15000 });
- const { monitorType, isTLSEnabled, isZipUrlTLSEnabled } = usePolicyConfigContext();
- const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext();
- const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext();
- const { fields: icmpSimpleFields } = useICMPSimpleFieldsContext();
- const { fields: httpAdvancedFields } = useHTTPAdvancedFieldsContext();
- const { fields: tcpAdvancedFields } = useTCPAdvancedFieldsContext();
- const { fields: tlsFields } = useTLSFieldsContext();
- const { fields: browserSimpleFields } = useBrowserSimpleFieldsContext();
- const { fields: browserAdvancedFields } = useBrowserAdvancedFieldsContext();
- const metadata = useMemo(
- () => ({
- is_tls_enabled: isTLSEnabled,
- is_zip_url_tls_enabled: isZipUrlTLSEnabled,
- }),
- [isTLSEnabled, isZipUrlTLSEnabled]
- );
-
- const policyConfig: PolicyConfig = {
- [DataStream.HTTP]: {
- ...httpSimpleFields,
- ...httpAdvancedFields,
- ...tlsFields,
- [ConfigKeys.METADATA]: metadata,
- [ConfigKeys.NAME]: newPolicy.name,
- } as HTTPFields,
- [DataStream.TCP]: {
- ...tcpSimpleFields,
- ...tcpAdvancedFields,
- ...tlsFields,
- [ConfigKeys.METADATA]: metadata,
- [ConfigKeys.NAME]: newPolicy.name,
- } as TCPFields,
- [DataStream.ICMP]: {
- ...icmpSimpleFields,
- [ConfigKeys.NAME]: newPolicy.name,
- } as ICMPFields,
- [DataStream.BROWSER]: {
- ...browserSimpleFields,
- ...browserAdvancedFields,
- [ConfigKeys.METADATA]: metadata,
- [ConfigKeys.NAME]: newPolicy.name,
- } as BrowserFields,
- };
+ const { monitorType } = usePolicyConfigContext();
+ const policyConfig: PolicyConfig = usePolicy(newPolicy.name);
useUpdatePolicy({
defaultConfig,
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx
index e874ca73d951b..c141e33ea1595 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx
@@ -1075,6 +1075,59 @@ describe('', () => {
expect(queryByLabelText('Host')).not.toBeInTheDocument();
});
+ it.each([
+ [true, 'Testing script'],
+ [false, 'Inline script'],
+ ])(
+ 'browser monitors - auto selects the right tab depending on source metadata',
+ async (isGeneratedScript, text) => {
+ const currentPolicy = {
+ ...defaultCurrentPolicy,
+ inputs: [
+ {
+ ...defaultNewPolicy.inputs[0],
+ enabled: false,
+ },
+ {
+ ...defaultNewPolicy.inputs[1],
+ enabled: false,
+ },
+ {
+ ...defaultNewPolicy.inputs[2],
+ enabled: false,
+ },
+ {
+ ...defaultNewPolicy.inputs[3],
+ enabled: true,
+ streams: [
+ {
+ ...defaultNewPolicy.inputs[3].streams[0],
+ vars: {
+ ...defaultNewPolicy.inputs[3].streams[0].vars,
+ 'source.inline.script': {
+ type: 'yaml',
+ value: JSON.stringify('step(() => {})'),
+ },
+ __ui: {
+ type: 'yaml',
+ value: JSON.stringify({
+ script_source: {
+ is_generated_script: isGeneratedScript,
+ },
+ }),
+ },
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ const { getByText } = render();
+
+ expect(getByText(text)).toBeInTheDocument();
+ }
+ );
it('hides tls fields when metadata.is_tls_enabled is false', async () => {
const { getByLabelText, queryByLabelText } = render(
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx
index f391c6c271f69..13296ee4521cd 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx
@@ -129,6 +129,10 @@ export enum ConfigKeys {
export interface Metadata {
is_tls_enabled?: boolean;
is_zip_url_tls_enabled?: boolean;
+ script_source?: {
+ is_generated_script: boolean;
+ file_name: string;
+ };
}
export interface ICommonFields {
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx
index 3824b9ae19d0f..c73fce07f9779 100644
--- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx
@@ -52,7 +52,7 @@ export const WaterfallChartFixedAxis = ({ tickFormat, domain, barStyleAccessor }
{
+ // Failing: See https://github.com/elastic/kibana/issues/115666
+ describe.skip('ml', () => {
const esArchiver = getService('esArchiver');
before(async () => {
diff --git a/x-pack/test/api_integration/apis/maps/fonts_api.js b/x-pack/test/api_integration/apis/maps/fonts_api.js
index afde003b05f2d..35017e6e37db8 100644
--- a/x-pack/test/api_integration/apis/maps/fonts_api.js
+++ b/x-pack/test/api_integration/apis/maps/fonts_api.js
@@ -6,11 +6,33 @@
*/
import expect from '@kbn/expect';
+import path from 'path';
+import { copyFile, rm } from 'fs/promises';
export default function ({ getService }) {
const supertest = getService('supertest');
+ const log = getService('log');
describe('fonts', () => {
+ // [HACK]: On CI tests are run from the different directories than the built and running Kibana
+ // instance. To workaround that we use Kibana `process.cwd()` to construct font path manually.
+ // x-pack tests can be run from root directory or from within x-pack so need to cater for both possibilities.
+ const fontPath = path.join(
+ process.cwd().replace(/x-pack.*$/, ''),
+ 'x-pack/plugins/maps/server/fonts/open_sans/0-255.pbf'
+ );
+ const destinationPath = path.join(path.dirname(fontPath), '..', path.basename(fontPath));
+
+ before(async () => {
+ log.debug(`Copying test file from '${fontPath}' to '${destinationPath}'`);
+ await copyFile(fontPath, destinationPath);
+ });
+
+ after(async () => {
+ log.debug(`Removing test file '${destinationPath}'`);
+ await rm(destinationPath);
+ });
+
it('should return fonts', async () => {
const resp = await supertest
.get(`/api/maps/fonts/Open%20Sans%20Regular,Arial%20Unicode%20MS%20Regular/0-255`)
@@ -25,12 +47,12 @@ export default function ({ getService }) {
.expect(404);
});
- it('should return 404 when file is not in font folder (../)', async () => {
- await supertest.get(`/api/maps/fonts/open_sans/..%2fopen_sans%2f0-255`).expect(404);
+ it('should return 404 when file is not in font folder (..)', async () => {
+ await supertest.get(`/api/maps/fonts/open_sans/..%2f0-255`).expect(404);
});
- it('should return 404 when file is not in font folder (./../)', async () => {
- await supertest.get(`/api/maps/fonts/open_sans/.%2f..%2fopen_sans%2f0-255`).expect(404);
+ it('should return 404 when file is not in font folder (./..)', async () => {
+ await supertest.get(`/api/maps/fonts/open_sans/.%2f..%2f0-255`).expect(404);
});
});
}
diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts
index 1071e9fae3417..03b1beffa7993 100644
--- a/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts
+++ b/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts
@@ -76,7 +76,13 @@ export default ({ getService }: FtrProviderContext): void => {
const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[1]);
expect(bodySplitAndParsed).to.eql({
- exported_count: 1,
+ exported_exception_list_count: 0,
+ exported_exception_list_item_count: 0,
+ exported_rules_count: 1,
+ missing_exception_list_item_count: 0,
+ missing_exception_list_items: [],
+ missing_exception_lists: [],
+ missing_exception_lists_count: 0,
missing_rules: [],
missing_rules_count: 0,
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts
index 9b7c75bab3100..3c033d2077c54 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts
@@ -51,9 +51,9 @@ export default ({ getService }: FtrProviderContext) => {
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 4, [id]);
const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map(
- (signal) => (signal._source?.host_alias as HostAlias).name
- );
+ const hits = signalsOpen.hits.hits
+ .map((signal) => (signal._source?.host_alias as HostAlias).name)
+ .sort();
expect(hits).to.eql(['host name 1', 'host name 2', 'host name 3', 'host name 4']);
});
@@ -63,7 +63,9 @@ export default ({ getService }: FtrProviderContext) => {
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 4, [id]);
const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((signal) => (signal._source?.host as HostAlias).name);
+ const hits = signalsOpen.hits.hits
+ .map((signal) => (signal._source?.host as HostAlias).name)
+ .sort();
expect(hits).to.eql(['host name 1', 'host name 2', 'host name 3', 'host name 4']);
});
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts
index b0f208aadaf1b..6d04ffc67c573 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts
@@ -7,6 +7,7 @@
import expect from '@kbn/expect';
+import type SuperTest from 'supertest';
import {
createListsIndex,
deleteAllExceptions,
@@ -25,6 +26,45 @@ import {
waitForSignalsToBePresent,
} from '../../utils';
+interface Host {
+ os: {
+ type?: string;
+ name?: string;
+ };
+}
+
+/**
+ * Convenience method to get signals by host and sort them for better deterministic testing
+ * since Elastic can return the hits back in any order we want to sort them on return for testing.
+ * @param supertest Super test for testing.
+ * @param id The signals id
+ * @returns The array of hosts sorted
+ */
+export const getHostHits = async (
+ supertest: SuperTest.SuperTest,
+ id: string
+): Promise => {
+ const signalsOpen = await getSignalsById(supertest, id);
+ return signalsOpen.hits.hits
+ .map((hit) => hit._source?.host as Host)
+ .sort((a, b) => {
+ let sortOrder = 0;
+ if (a.os.name != null && b.os.name != null) {
+ sortOrder += a.os.name.localeCompare(b.os.name);
+ }
+ if (a.os.type != null && b.os.type != null) {
+ sortOrder += a.os.type.localeCompare(b.os.type);
+ }
+ if (a.os.type != null && b.os.name != null) {
+ sortOrder += a.os.type.localeCompare(b.os.name);
+ }
+ if (a.os.name != null && b.os.type != null) {
+ sortOrder += a.os.name.localeCompare(b.os.type);
+ }
+ return sortOrder;
+ });
+};
+
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
@@ -64,20 +104,19 @@ export default ({ getService }: FtrProviderContext) => {
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 4, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host).sort();
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
os: { type: 'linux' },
},
{
- os: { type: 'windows' },
+ os: { type: 'linux' },
},
{
os: { type: 'macos' },
},
{
- os: { type: 'linux' },
+ os: { type: 'windows' },
},
]);
});
@@ -87,20 +126,19 @@ export default ({ getService }: FtrProviderContext) => {
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 4, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host).sort();
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
os: { name: 'Linux' },
},
{
- os: { name: 'Windows' },
+ os: { name: 'Linux' },
},
{
os: { name: 'Macos' },
},
{
- os: { name: 'Linux' },
+ os: { name: 'Windows' },
},
]);
});
@@ -130,17 +168,16 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 3, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { name: 'Windows' },
+ os: { name: 'Linux' },
},
{
os: { name: 'Macos' },
},
{
- os: { name: 'Linux' },
+ os: { name: 'Windows' },
},
]);
});
@@ -167,17 +204,16 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 3, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { name: 'Windows' },
+ os: { name: 'Linux' },
},
{
os: { name: 'Macos' },
},
{
- os: { name: 'Linux' },
+ os: { name: 'Windows' },
},
]);
});
@@ -215,14 +251,13 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 2, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { name: 'Macos' },
+ os: { name: 'Linux' },
},
{
- os: { name: 'Linux' },
+ os: { name: 'Macos' },
},
]);
});
@@ -260,14 +295,13 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 2, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { name: 'Macos' },
+ os: { name: 'Linux' },
},
{
- os: { name: 'Linux' },
+ os: { name: 'Macos' },
},
]);
});
@@ -296,17 +330,16 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 3, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { type: 'windows' },
+ os: { type: 'linux' },
},
{
os: { type: 'macos' },
},
{
- os: { type: 'linux' },
+ os: { type: 'windows' },
},
]);
});
@@ -333,17 +366,16 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 3, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { type: 'windows' },
+ os: { type: 'linux' },
},
{
os: { type: 'macos' },
},
{
- os: { type: 'linux' },
+ os: { type: 'windows' },
},
]);
});
@@ -381,14 +413,13 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 2, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { type: 'macos' },
+ os: { type: 'linux' },
},
{
- os: { type: 'linux' },
+ os: { type: 'macos' },
},
]);
});
@@ -426,14 +457,13 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 2, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { type: 'macos' },
+ os: { type: 'linux' },
},
{
- os: { type: 'linux' },
+ os: { type: 'macos' },
},
]);
});
@@ -462,14 +492,13 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 6, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { type: 'windows' },
+ os: { type: 'linux' },
},
{
- os: { name: 'Windows' },
+ os: { name: 'Linux' },
},
{
os: { type: 'macos' },
@@ -478,10 +507,10 @@ export default ({ getService }: FtrProviderContext) => {
os: { name: 'Macos' },
},
{
- os: { type: 'linux' },
+ os: { type: 'windows' },
},
{
- os: { name: 'Linux' },
+ os: { name: 'Windows' },
},
]);
});
@@ -508,14 +537,13 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 6, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { type: 'windows' },
+ os: { type: 'linux' },
},
{
- os: { name: 'Windows' },
+ os: { name: 'Linux' },
},
{
os: { type: 'macos' },
@@ -524,10 +552,10 @@ export default ({ getService }: FtrProviderContext) => {
os: { name: 'Macos' },
},
{
- os: { type: 'linux' },
+ os: { type: 'windows' },
},
{
- os: { name: 'Linux' },
+ os: { name: 'Windows' },
},
]);
});
@@ -565,20 +593,19 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 4, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { type: 'macos' },
+ os: { type: 'linux' },
},
{
- os: { name: 'Macos' },
+ os: { name: 'Linux' },
},
{
- os: { type: 'linux' },
+ os: { type: 'macos' },
},
{
- os: { name: 'Linux' },
+ os: { name: 'Macos' },
},
]);
});
@@ -616,20 +643,19 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 4, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { type: 'macos' },
+ os: { type: 'linux' },
},
{
- os: { name: 'Macos' },
+ os: { name: 'Linux' },
},
{
- os: { type: 'linux' },
+ os: { type: 'macos' },
},
{
- os: { name: 'Linux' },
+ os: { name: 'Macos' },
},
]);
});
@@ -668,8 +694,7 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
os: { type: 'macos' },
@@ -708,8 +733,7 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
os: { type: 'macos' },
@@ -741,17 +765,16 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 3, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
os: { type: 'linux' },
},
{
- os: { type: 'macos' },
+ os: { type: 'linux' },
},
{
- os: { type: 'linux' },
+ os: { type: 'macos' },
},
]);
});
@@ -778,14 +801,13 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 2, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { type: 'macos' },
+ os: { type: 'linux' },
},
{
- os: { type: 'linux' },
+ os: { type: 'macos' },
},
]);
});
@@ -812,14 +834,13 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 2, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
- os: { type: 'macos' },
+ os: { type: 'linux' },
},
{
- os: { type: 'linux' },
+ os: { type: 'macos' },
},
]);
});
@@ -846,20 +867,19 @@ export default ({ getService }: FtrProviderContext) => {
);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 4, [id]);
- const signalsOpen = await getSignalsById(supertest, id);
- const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host);
+ const hits = await getHostHits(supertest, id);
expect(hits).to.eql([
{
os: { type: 'linux' },
},
{
- os: { type: 'windows' },
+ os: { type: 'linux' },
},
{
os: { type: 'macos' },
},
{
- os: { type: 'linux' },
+ os: { type: 'windows' },
},
]);
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts
index f7208a8832c4d..912596ed7ca00 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts
@@ -499,7 +499,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.float).sort();
expect(hits).to.eql(['1.1', '1.2', '1.3']);
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts
index 42152fd18473a..da9219e4b52f6 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts
@@ -501,7 +501,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.integer).sort();
expect(hits).to.eql(['2', '3', '4']);
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts
index 147e6058dffa8..526c6d1c988ce 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts
@@ -151,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForSignalsToBePresent(supertest, 1, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort();
- expect(ips).to.eql([[]]);
+ expect(ips.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
it('should filter a CIDR range of "127.0.0.1/30"', async () => {
@@ -167,7 +167,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort();
expect(ips).to.eql([
@@ -190,7 +190,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 2, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort();
expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]);
@@ -346,7 +346,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForSignalsToBePresent(supertest, 1, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort();
- expect(ips).to.eql([[]]);
+ expect(ips.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
});
@@ -392,8 +392,7 @@ export default ({ getService }: FtrProviderContext) => {
});
});
- // FLAKY: https://github.com/elastic/kibana/issues/115315
- describe.skip('"exists" operator', () => {
+ describe('"exists" operator', () => {
it('will return 1 empty result if matching against ip', async () => {
const rule = getRuleForSignalTesting(['ip_as_array']);
const { id } = await createRuleWithExceptionEntries(supertest, rule, [
@@ -408,7 +407,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsById(supertest, id);
const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort();
- expect(ips).to.eql([[]]);
+ expect(ips.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
});
@@ -487,8 +486,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]);
});
- // FLAKY https://github.com/elastic/kibana/issues/89052
- it.skip('will return 1 result if we have a list that includes all ips', async () => {
+ it('will return 1 result if we have a list that includes all ips', async () => {
await importFile(
supertest,
'ip',
@@ -512,7 +510,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsById(supertest, id);
const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort();
- expect(ips).to.eql([[]]);
+ expect(ips.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
it('will return 2 results if we have a list which contains the CIDR ranges of "127.0.0.1/32, 127.0.0.2/31, 127.0.0.4/30"', async () => {
@@ -546,7 +544,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 2, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort();
expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]);
@@ -577,7 +575,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 2, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort();
expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]);
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts
index e852558aaa6a8..8571aa8eeaa60 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts
@@ -60,7 +60,7 @@ export default ({ getService }: FtrProviderContext) => {
const rule = getRuleForSignalTesting(['keyword_as_array']);
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 3, [id]);
+ await waitForSignalsToBePresent(supertest, 4, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
expect(hits).to.eql([
@@ -84,7 +84,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 2, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
expect(hits).to.eql([
@@ -153,7 +153,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForSignalsToBePresent(supertest, 1, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
- expect(hits).to.eql([[]]);
+ expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
});
@@ -281,7 +281,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForSignalsToBePresent(supertest, 1, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
- expect(hits).to.eql([[]]);
+ expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
});
@@ -328,8 +328,7 @@ export default ({ getService }: FtrProviderContext) => {
});
describe('"exists" operator', () => {
- // FLAKY https://github.com/elastic/kibana/issues/115308
- it.skip('will return 1 results if matching against keyword for the empty array', async () => {
+ it('will return 1 results if matching against keyword for the empty array', async () => {
const rule = getRuleForSignalTesting(['keyword_as_array']);
const { id } = await createRuleWithExceptionEntries(supertest, rule, [
[
@@ -343,7 +342,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
- expect(hits).to.eql([[]]);
+ expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
});
@@ -399,7 +398,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 3, [id]);
+ await waitForSignalsToBePresent(supertest, 4, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
expect(hits).to.eql([
@@ -437,7 +436,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 2, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
expect(hits).to.eql([
@@ -497,8 +496,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]);
});
- // FLAKY https://github.com/elastic/kibana/issues/115304
- it.skip('will return only the empty array for results if we have a list that includes all keyword', async () => {
+ it('will return only the empty array for results if we have a list that includes all keyword', async () => {
await importFile(
supertest,
'keyword',
@@ -522,7 +520,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
- expect(hits).to.eql([[]]);
+ expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts
index 35573edea3c39..8d5f1515e4ab6 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts
@@ -499,7 +499,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.long).sort();
expect(hits).to.eql(['2', '3', '4']);
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts
index 4e4823fcf747f..367e68f7f9ed1 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts
@@ -56,8 +56,7 @@ export default ({ getService }: FtrProviderContext) => {
await deleteListsIndex(supertest);
});
- // FLAKY: https://github.com/elastic/kibana/issues/115310
- describe.skip('"is" operator', () => {
+ describe('"is" operator', () => {
it('should find all the text from the data set when no exceptions are set on the rule', async () => {
const rule = getRuleForSignalTesting(['text']);
const { id } = await createRule(supertest, rule);
@@ -241,7 +240,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
expect(hits).to.eql(['word four', 'word three', 'word two']);
@@ -344,6 +343,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
+ await waitForSignalsToBePresent(supertest, 4, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']);
@@ -618,7 +618,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
expect(hits).to.eql(['word four', 'word three', 'word two']);
@@ -646,7 +646,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
expect(hits).to.eql(['word four', 'word three', 'word two']);
@@ -669,7 +669,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 2, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
expect(hits).to.eql(['word four', 'word two']);
@@ -850,7 +850,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 2, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
expect(hits).to.eql(['word one', 'word three']);
@@ -878,7 +878,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 1, [id]);
+ await waitForSignalsToBePresent(supertest, 4, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']);
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts
index f0a5fe7c1ffb1..3eedabd41d663 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts
@@ -58,7 +58,7 @@ export default ({ getService }: FtrProviderContext) => {
const rule = getRuleForSignalTesting(['text_as_array']);
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 3, [id]);
+ await waitForSignalsToBePresent(supertest, 4, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
expect(hits).to.eql([
@@ -82,7 +82,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 2, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
expect(hits).to.eql([
@@ -151,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForSignalsToBePresent(supertest, 1, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
- expect(hits).to.eql([[]]);
+ expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
});
@@ -279,7 +279,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForSignalsToBePresent(supertest, 1, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
- expect(hits).to.eql([[]]);
+ expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
});
@@ -326,8 +326,7 @@ export default ({ getService }: FtrProviderContext) => {
});
describe('"exists" operator', () => {
- // FLAKY https://github.com/elastic/kibana/issues/115313
- it.skip('will return 1 results if matching against text for the empty array', async () => {
+ it('will return 1 results if matching against text for the empty array', async () => {
const rule = getRuleForSignalTesting(['text_as_array']);
const { id } = await createRuleWithExceptionEntries(supertest, rule, [
[
@@ -341,7 +340,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
- expect(hits).to.eql([[]]);
+ expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
});
@@ -435,7 +434,7 @@ export default ({ getService }: FtrProviderContext) => {
],
]);
await waitForRuleSuccessOrStatus(supertest, id);
- await waitForSignalsToBePresent(supertest, 2, [id]);
+ await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
expect(hits).to.eql([
@@ -495,8 +494,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]);
});
- // FLAKY https://github.com/elastic/kibana/issues/113418
- it.skip('will return only the empty array for results if we have a list that includes all text', async () => {
+ it('will return only the empty array for results if we have a list that includes all text', async () => {
await importFile(
supertest,
'text',
@@ -520,7 +518,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsById(supertest, id);
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort();
- expect(hits).to.eql([[]]);
+ expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
});
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts
index 1071e9fae3417..03b1beffa7993 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts
@@ -76,7 +76,13 @@ export default ({ getService }: FtrProviderContext): void => {
const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[1]);
expect(bodySplitAndParsed).to.eql({
- exported_count: 1,
+ exported_exception_list_count: 0,
+ exported_exception_list_item_count: 0,
+ exported_rules_count: 1,
+ missing_exception_list_item_count: 0,
+ missing_exception_list_items: [],
+ missing_exception_lists: [],
+ missing_exception_lists_count: 0,
missing_rules: [],
missing_rules_count: 0,
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts
index 53613624067e1..83166619b152d 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts
@@ -57,7 +57,13 @@ export default ({ getService }: FtrProviderContext): void => {
const exportDetails = JSON.parse(exportDetailsJson);
expect(exportDetails).to.eql({
- exported_count: 1,
+ exported_exception_list_count: 0,
+ exported_exception_list_item_count: 0,
+ exported_rules_count: 1,
+ missing_exception_list_item_count: 0,
+ missing_exception_list_items: [],
+ missing_exception_lists: [],
+ missing_exception_lists_count: 0,
missing_rules: [],
missing_rules_count: 0,
});
diff --git a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts b/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts
index e244c907a76d6..a0b3c636a2f1a 100644
--- a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts
+++ b/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts
@@ -34,8 +34,7 @@ export default function ({ getService }: FtrProviderContext) {
},
};
- // FLAKY https://github.com/elastic/kibana/issues/113890
- describe.skip('creation with runtime mappings', function () {
+ describe('creation with runtime mappings', function () {
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
await transform.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp');
diff --git a/x-pack/test/functional/services/ml/dashboard_embeddables.ts b/x-pack/test/functional/services/ml/dashboard_embeddables.ts
index c4fa9f643e69e..0dc5cc8fae2d5 100644
--- a/x-pack/test/functional/services/ml/dashboard_embeddables.ts
+++ b/x-pack/test/functional/services/ml/dashboard_embeddables.ts
@@ -7,12 +7,10 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
-import { MlCommonUI } from './common_ui';
import { MlDashboardJobSelectionTable } from './dashboard_job_selection_table';
export function MachineLearningDashboardEmbeddablesProvider(
{ getService }: FtrProviderContext,
- mlCommonUI: MlCommonUI,
mlDashboardJobSelectionTable: MlDashboardJobSelectionTable
) {
const retry = getService('retry');
@@ -22,14 +20,14 @@ export function MachineLearningDashboardEmbeddablesProvider(
return {
async assertAnomalyChartsEmbeddableInitializerExists() {
- await retry.tryForTime(5000, async () => {
- await testSubjects.existOrFail('mlAnomalyChartsEmbeddableInitializer');
+ await retry.tryForTime(10 * 1000, async () => {
+ await testSubjects.existOrFail('mlAnomalyChartsEmbeddableInitializer', { timeout: 1000 });
});
},
async assertAnomalyChartsEmbeddableInitializerNotExists() {
- await retry.tryForTime(5000, async () => {
- await testSubjects.missingOrFail('mlAnomalyChartsEmbeddableInitializer');
+ await retry.tryForTime(10 * 1000, async () => {
+ await testSubjects.missingOrFail('mlAnomalyChartsEmbeddableInitializer', { timeout: 1000 });
});
},
diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts
index 45ba4c5c34833..2c375d47b0b3b 100644
--- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts
+++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts
@@ -18,10 +18,11 @@ import {
} from '../../../../plugins/ml/common/util/analytics_utils';
export function MachineLearningDataFrameAnalyticsCreationProvider(
- { getService }: FtrProviderContext,
+ { getPageObject, getService }: FtrProviderContext,
mlCommonUI: MlCommonUI,
mlApi: MlApi
) {
+ const headerPage = getPageObject('header');
const testSubjects = getService('testSubjects');
const comboBox = getService('comboBox');
const retry = getService('retry');
@@ -111,10 +112,12 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
},
async assertSourceDataPreviewExists() {
+ await headerPage.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('mlAnalyticsCreationDataGrid loaded', { timeout: 5000 });
},
async assertIndexPreviewHistogramChartButtonExists() {
+ await headerPage.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('mlAnalyticsCreationDataGridHistogramButton');
},
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 ac728e6b88303..a1bf8c6a65d70 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
@@ -73,7 +73,7 @@ export function MachineLearningDataFrameAnalyticsResultsProvider(
async assertTotalFeatureImportanceEvaluatePanelExists() {
await testSubjects.existOrFail('mlDFExpandableSection-FeatureImportanceSummary');
- await testSubjects.existOrFail('mlTotalFeatureImportanceChart', { timeout: 5000 });
+ await testSubjects.existOrFail('mlTotalFeatureImportanceChart', { timeout: 30 * 1000 });
},
async assertFeatureImportanceDecisionPathElementsExists() {
@@ -167,17 +167,19 @@ export function MachineLearningDataFrameAnalyticsResultsProvider(
async openFeatureImportancePopover() {
this.assertResultsTableNotEmpty();
- const featureImportanceCell = await this.getFirstFeatureImportanceCell();
- await featureImportanceCell.focus();
- const interactionButton = await featureImportanceCell.findByTagName('button');
+ await retry.tryForTime(30 * 1000, async () => {
+ const featureImportanceCell = await this.getFirstFeatureImportanceCell();
+ await featureImportanceCell.focus();
+ const interactionButton = await featureImportanceCell.findByTagName('button');
- // simulate hover and wait for button to appear
- await featureImportanceCell.moveMouseTo();
- await this.waitForInteractionButtonToDisplay(interactionButton);
+ // simulate hover and wait for button to appear
+ await featureImportanceCell.moveMouseTo();
+ await this.waitForInteractionButtonToDisplay(interactionButton);
- // open popover
- await interactionButton.click();
- await testSubjects.existOrFail('mlDFAFeatureImportancePopover');
+ // open popover
+ await interactionButton.click();
+ await testSubjects.existOrFail('mlDFAFeatureImportancePopover', { timeout: 1000 });
+ });
},
async getFirstFeatureImportanceCell(): Promise {
diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts
index 5850919f2adc3..0bfb37c6c94f8 100644
--- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts
+++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts
@@ -196,6 +196,11 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F
analyticsId: string,
shouldBeDisplayed: boolean
) {
+ await this.waitForRefreshButtonLoaded();
+ await testSubjects.click('~mlAnalyticsRefreshListButton');
+ await this.waitForRefreshButtonLoaded();
+ await testSubjects.existOrFail('mlAnalyticsJobList', { timeout: 30 * 1000 });
+
if (shouldBeDisplayed) {
await this.filterWithSearchString(analyticsId, 1);
} else {
diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts
index 7f32968ec4326..6883946452629 100644
--- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts
+++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts
@@ -33,8 +33,11 @@ export function MachineLearningDataVisualizerIndexBasedProvider({
},
async clickUseFullDataButton(expectedFormattedTotalDocCount: string) {
- await testSubjects.clickWhenNotDisabled('dataVisualizerButtonUseFullData');
- await this.assertTotalDocumentCount(expectedFormattedTotalDocCount);
+ await retry.tryForTime(30 * 1000, async () => {
+ await testSubjects.clickWhenNotDisabled('dataVisualizerButtonUseFullData');
+ await testSubjects.clickWhenNotDisabled('superDatePickerApplyTimeButton');
+ await this.assertTotalDocumentCount(expectedFormattedTotalDocCount);
+ });
},
async assertTotalDocCountHeaderExist() {
diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts
index d50ec371d7c23..17302b2782223 100644
--- a/x-pack/test/functional/services/ml/index.ts
+++ b/x-pack/test/functional/services/ml/index.ts
@@ -67,7 +67,6 @@ export function MachineLearningProvider(context: FtrProviderContext) {
const dashboardJobSelectionTable = MachineLearningDashboardJobSelectionTableProvider(context);
const dashboardEmbeddables = MachineLearningDashboardEmbeddablesProvider(
context,
- commonUI,
dashboardJobSelectionTable
);
diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/export_exception_list.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/export_exception_list.ts
index d35d34fde5bcc..c21026d5df3d2 100644
--- a/x-pack/test/lists_api_integration/security_and_spaces/tests/export_exception_list.ts
+++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/export_exception_list.ts
@@ -77,7 +77,7 @@ export default ({ getService }: FtrProviderContext): void => {
.expect(400);
expect(exportBody).to.eql({
- message: 'exception list with list_id: not_exist does not exist',
+ message: 'exception list with list_id: not_exist or id: not_exist does not exist',
status_code: 400,
});
});
diff --git a/yarn.lock b/yarn.lock
index 375e748564283..8d174464d0a92 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6985,10 +6985,10 @@
resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f"
integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA==
-"@types/selenium-webdriver@^4.0.9":
- version "4.0.9"
- resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.9.tgz#12621e55b2ef8f6c98bd17fe23fa720c6cba16bd"
- integrity sha512-HopIwBE7GUXsscmt/J0DhnFXLSmO04AfxT6b8HAprknwka7pqEWquWDMXxCjd+NUHK9MkCe1SDKKsMiNmCItbQ==
+"@types/selenium-webdriver@^4.0.15":
+ version "4.0.15"
+ resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.15.tgz#03012b84155cf6bbae2f64aa9dccf2a84c78c7c8"
+ integrity sha512-5760PIZkzhPejy3hsKAdCKe5LJygGdxLKOLxmZL9GEUcFlO5OgzM6G2EbdbvOnaw4xvUSa9Uip6Ipwkih12BPA==
"@types/semver@^7":
version "7.3.4"
@@ -18808,10 +18808,10 @@ jsts@^1.6.2:
array-includes "^3.1.2"
object.assign "^4.1.2"
-jszip@^3.2.2:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.3.0.tgz#29d72c21a54990fa885b11fc843db320640d5271"
- integrity sha512-EJ9k766htB1ZWnsV5ZMDkKLgA+201r/ouFF8R2OigVjVdcm2rurcBrrdXaeqBJbqnUVMko512PYmlncBKE1Huw==
+jszip@^3.6.0:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9"
+ integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
@@ -21737,7 +21737,7 @@ os-shim@^0.1.2:
resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917"
integrity sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=
-os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
+os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
@@ -25397,13 +25397,6 @@ rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3:
dependencies:
glob "^7.1.3"
-rimraf@^2.7.1:
- version "2.7.1"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
- integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
- dependencies:
- glob "^7.1.3"
-
rimraf@~2.4.0:
version "2.4.5"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da"
@@ -25709,14 +25702,15 @@ select-hose@^2.0.0:
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
-selenium-webdriver@^4.0.0-alpha.7:
- version "4.0.0-alpha.7"
- resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.7.tgz#e3879d8457fd7ad8e4424094b7dc0540d99e6797"
- integrity sha512-D4qnTsyTr91jT8f7MfN+OwY0IlU5+5FmlO5xlgRUV6hDEV8JyYx2NerdTEqDDkNq7RZDYc4VoPALk8l578RBHw==
+selenium-webdriver@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0.tgz#7dc8969facee3be634459e173f557b7e34308e73"
+ integrity sha512-tOlu6FnTjPq2FKpd153pl8o2cB7H40Rvl/ogiD2sapMv4IDjQqpIxbd+swDJe9UDLdszeh5CDis6lgy4e9UG1w==
dependencies:
- jszip "^3.2.2"
- rimraf "^2.7.1"
- tmp "0.0.30"
+ jszip "^3.6.0"
+ rimraf "^3.0.2"
+ tmp "^0.2.1"
+ ws ">=7.4.6"
selfsigned@^1.10.7:
version "1.10.8"
@@ -27871,13 +27865,6 @@ title-case@^2.1.1:
no-case "^2.2.0"
upper-case "^1.0.3"
-tmp@0.0.30:
- version "0.0.30"
- resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed"
- integrity sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=
- dependencies:
- os-tmpdir "~1.0.1"
-
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -27885,7 +27872,7 @@ tmp@^0.0.33:
dependencies:
os-tmpdir "~1.0.2"
-tmp@~0.2.1:
+tmp@^0.2.1, tmp@~0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
@@ -30289,6 +30276,11 @@ write-pkg@^4.0.0:
type-fest "^0.4.1"
write-json-file "^3.2.0"
+ws@>=7.4.6, ws@^7.4.6:
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
+ integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
+
ws@^6.1.2, ws@^6.2.1:
version "6.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
@@ -30301,11 +30293,6 @@ ws@^7.2.3:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
-ws@^7.4.6:
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
- integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
-
x-is-function@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/x-is-function/-/x-is-function-1.0.4.tgz#5d294dc3d268cbdd062580e0c5df77a391d1fa1e"