diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 3a7c50feb38b7..f786d01232227 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -116,7 +116,6 @@ pageLoadAssetSize: dataViewManagement: 5000 reporting: 57003 visTypeHeatmap: 25340 - screenshotting: 17017 expressionGauge: 25000 controls: 34788 expressionPartitionVis: 26338 diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap index affd547dab0cd..d242dfb4cf42a 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap @@ -40,6 +40,7 @@ Object { ], }, "metric": Object { + "autoScale": undefined, "labels": Object { "position": "bottom", "show": true, diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts index 5bf59e3bdb7a0..2766b5475ceb6 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts @@ -86,6 +86,13 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ defaultMessage: 'bucket dimension configuration', }), }, + autoScale: { + types: ['boolean'], + help: i18n.translate('expressionMetricVis.function.autoScale.help', { + defaultMessage: 'Enable auto scale', + }), + required: false, + }, }, fn(input, args, handlers) { if (args.percentageMode && !args.palette?.params) { @@ -136,6 +143,7 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ labelColor: args.colorMode === ColorMode.Labels, ...args.font, }, + autoScale: args.autoScale, }, dimensions: { metrics: args.metric, diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts index 33d588d31aae9..bbc409e4dea0d 100644 --- a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts @@ -27,6 +27,7 @@ export interface MetricArguments { labelPosition: 'bottom' | 'top'; metric: ExpressionValueVisDimension[]; bucket?: ExpressionValueVisDimension; + autoScale?: boolean; } export type MetricInput = Datatable; diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts index 74a5963702ea9..e35e50c65ef8e 100644 --- a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts @@ -30,6 +30,7 @@ export interface MetricVisParam { palette?: CustomPaletteState; labels: Labels & { style: Style; position: 'bottom' | 'top' }; style: MetricStyle; + autoScale?: boolean; } export interface VisParams { diff --git a/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/with_auto_scale.test.tsx.snap b/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/with_auto_scale.test.tsx.snap new file mode 100644 index 0000000000000..4acbab635ba47 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/with_auto_scale.test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AutoScale withAutoScale renders 1`] = ` + +
+
+ +

+ Hoi! +

+
+
+
+
+`; diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric.scss b/src/plugins/chart_expressions/expression_metric/public/components/metric.scss index 03dffb5af9760..377d2cdc1a02f 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric.scss +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric.scss @@ -6,6 +6,7 @@ // mtrChart__legend-isLoading .mtrVis { + @include euiScrollBar; height: 100%; width: 100%; display: flex; @@ -13,6 +14,7 @@ justify-content: center; align-items: center; flex-wrap: wrap; + overflow: auto; } .mtrVis__value { diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx index 9264022ad92df..2bde9c84db4df 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx @@ -17,6 +17,7 @@ import { ExpressionValueVisDimension } from '../../../../visualizations/public'; import { formatValue, shouldApplyColor } from '../utils'; import { getColumnByAccessor } from '../utils/accessor'; import { needsLightText } from '../utils/palette'; +import { withAutoScale } from './with_auto_scale'; import './metric.scss'; @@ -27,6 +28,8 @@ export interface MetricVisComponentProps { renderComplete: () => void; } +const AutoScaleMetricVisValue = withAutoScale(MetricVisValue); + class MetricVisComponent extends Component { private getColor(value: number, paletteParams: CustomPaletteState) { return getPaletteService().get('custom')?.getColorForValue?.(value, paletteParams, { @@ -108,8 +111,12 @@ class MetricVisComponent extends Component { }; private renderMetric = (metric: MetricOptions, index: number) => { + const MetricComponent = this.props.visParams.metric.autoScale + ? AutoScaleMetricVisValue + : MetricVisValue; + return ( - ({ + clientHeight, + clientWidth, +}); + +describe('AutoScale', () => { + describe('computeScale', () => { + it('is 1 if any element is null', () => { + expect(computeScale(null, null)).toBe(1); + expect(computeScale(mockElement(), null)).toBe(1); + expect(computeScale(null, mockElement())).toBe(1); + }); + + it('is never over 1', () => { + expect(computeScale(mockElement(2000, 2000), mockElement(1000, 1000))).toBe(1); + }); + + it('is never under 0.3 in default case', () => { + expect(computeScale(mockElement(2000, 1000), mockElement(1000, 10000))).toBe(0.3); + }); + + it('is never under specified min scale if specified', () => { + expect(computeScale(mockElement(2000, 1000), mockElement(1000, 10000), 0.1)).toBe(0.1); + }); + + it('is the lesser of the x or y scale', () => { + expect(computeScale(mockElement(2000, 2000), mockElement(3000, 5000))).toBe(0.4); + expect(computeScale(mockElement(2000, 3000), mockElement(4000, 3200))).toBe(0.5); + }); + }); + + describe('withAutoScale', () => { + it('renders', () => { + const Component = () =>

Hoi!

; + const WrappedComponent = withAutoScale(Component); + expect(mount()).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/with_auto_scale.tsx b/src/plugins/chart_expressions/expression_metric/public/components/with_auto_scale.tsx new file mode 100644 index 0000000000000..241e882f34ba8 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/components/with_auto_scale.tsx @@ -0,0 +1,91 @@ +/* + * 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 React, { useRef, useEffect, useState, ComponentType, useMemo } from 'react'; +import { throttle } from 'lodash'; +import { useResizeObserver } from '@elastic/eui'; +import { autoScaleWrapperStyle } from './with_auto_scale.styles'; + +interface AutoScaleParams { + minScale?: number; +} +interface ClientDimensionable { + clientWidth: number; + clientHeight: number; +} + +const MAX_SCALE = 1; +const MIN_SCALE = 0.3; + +/** + * computeScale computes the ratio by which the child needs to shrink in order + * to fit into the parent. This function is only exported for testing purposes. + */ +export function computeScale( + parent: ClientDimensionable | null, + child: ClientDimensionable | null, + minScale: number = MIN_SCALE +) { + if (!parent || !child) { + return 1; + } + + const scaleX = parent.clientWidth / child.clientWidth; + const scaleY = parent.clientHeight / child.clientHeight; + + return Math.max(Math.min(MAX_SCALE, Math.min(scaleX, scaleY)), minScale); +} + +export function withAutoScale( + WrappedComponent: ComponentType, + autoScaleParams?: AutoScaleParams +) { + return (props: T) => { + // An initial scale of 0 means we always redraw + // at least once, which is sub-optimal, but it + // prevents an annoying flicker. + const [scale, setScale] = useState(0); + const parentRef = useRef(null); + const childrenRef = useRef(null); + const parentDimensions = useResizeObserver(parentRef.current); + + const scaleFn = useMemo( + () => + throttle(() => { + const newScale = computeScale( + { clientHeight: parentDimensions.height, clientWidth: parentDimensions.width }, + childrenRef.current, + autoScaleParams?.minScale + ); + + // Prevent an infinite render loop + if (scale !== newScale) { + setScale(newScale); + } + }), + [parentDimensions, setScale, scale] + ); + + useEffect(() => { + scaleFn(); + }, [scaleFn]); + + return ( +
+
+ +
+
+ ); + }; +} diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 2ce1c87252d38..98b3d761f350e 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -185,7 +185,7 @@ export const useDashboardAppState = ({ // Backwards compatible way of detecting that we are taking a screenshot const legacyPrintLayoutDetected = screenshotModeService?.isScreenshotMode() && - screenshotModeService.getScreenshotLayout() === 'print'; + screenshotModeService.getScreenshotContext('layout') === 'print'; const initialDashboardState = { ...savedDashboardState, diff --git a/src/plugins/dashboard/public/services/screenshot_mode.ts b/src/plugins/dashboard/public/services/screenshot_mode.ts index 12ec1bca2207f..577aa3eab36b5 100644 --- a/src/plugins/dashboard/public/services/screenshot_mode.ts +++ b/src/plugins/dashboard/public/services/screenshot_mode.ts @@ -10,5 +10,3 @@ export type { ScreenshotModePluginStart, ScreenshotModePluginSetup, } from '../../../screenshot_mode/public'; - -export type { Layout } from '../../../screenshot_mode/common'; diff --git a/src/plugins/screenshot_mode/common/context.test.ts b/src/plugins/screenshot_mode/common/context.test.ts new file mode 100644 index 0000000000000..370ea27907295 --- /dev/null +++ b/src/plugins/screenshot_mode/common/context.test.ts @@ -0,0 +1,30 @@ +/* + * 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 { getScreenshotContext, setScreenshotContext } from './context'; + +describe('getScreenshotContext', () => { + it('should return a default value if there is no data', () => { + expect(getScreenshotContext('key', 'default')).toBe('default'); + }); +}); + +describe('setScreenshotContext', () => { + it('should store data in the context', () => { + setScreenshotContext('key', 'value'); + + expect(getScreenshotContext('key')).toBe('value'); + }); + + it('should not overwrite data on repetitive calls', () => { + setScreenshotContext('key1', 'value1'); + setScreenshotContext('key2', 'value2'); + + expect(getScreenshotContext('key1')).toBe('value1'); + }); +}); diff --git a/src/plugins/screenshot_mode/common/context.ts b/src/plugins/screenshot_mode/common/context.ts new file mode 100644 index 0000000000000..7f885baa175dd --- /dev/null +++ b/src/plugins/screenshot_mode/common/context.ts @@ -0,0 +1,53 @@ +/* + * 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. + */ + +// **PLEASE NOTE** +// The functionality in this file targets a browser environment AND is intended to be used both in public and server. +// For instance, reporting uses these functions when starting puppeteer to set the current browser into "screenshot" mode. + +type Context = Record; + +declare global { + interface Window { + __KBN_SCREENSHOT_CONTEXT__?: Context; + } +} + +/** + * Stores a value in the screenshotting context. + * @param key Context key to set. + * @param value Value to set. + */ +export function setScreenshotContext(key: string, value: T): void { + // Literal value to prevent adding an external reference + const KBN_SCREENSHOT_CONTEXT = '__KBN_SCREENSHOT_CONTEXT__'; + + if (!window[KBN_SCREENSHOT_CONTEXT]) { + Object.defineProperty(window, KBN_SCREENSHOT_CONTEXT, { + enumerable: true, + writable: true, + configurable: false, + value: {}, + }); + } + + window[KBN_SCREENSHOT_CONTEXT]![key] = value; +} + +/** + * Retrieves a value from the screenshotting context. + * @param key Context key to get. + * @param defaultValue Value to return if the key is not found. + * @return The value stored in the screenshotting context. + */ +export function getScreenshotContext(key: string, defaultValue?: T): T | undefined { + // Literal value to prevent adding an external reference + const KBN_SCREENSHOT_CONTEXT = '__KBN_SCREENSHOT_CONTEXT__'; + + return (window[KBN_SCREENSHOT_CONTEXT]?.[key] as T) ?? defaultValue; +} diff --git a/src/plugins/screenshot_mode/common/index.ts b/src/plugins/screenshot_mode/common/index.ts index 949691911fc27..46f120b1ec1a9 100644 --- a/src/plugins/screenshot_mode/common/index.ts +++ b/src/plugins/screenshot_mode/common/index.ts @@ -6,16 +6,11 @@ * Side Public License, v 1. */ +export { getScreenshotContext, setScreenshotContext } from './context'; export { getScreenshotMode, setScreenshotModeEnabled, setScreenshotModeDisabled, KBN_SCREENSHOT_MODE_ENABLED_KEY, - KBN_SCREENSHOT_MODE_LAYOUT_KEY, - setScreenshotLayout, - getScreenshotLayout, -} from './get_set_browser_screenshot_mode'; - -export type { Layout } from './get_set_browser_screenshot_mode'; - +} from './mode'; export { KBN_SCREENSHOT_MODE_HEADER } from './constants'; diff --git a/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts b/src/plugins/screenshot_mode/common/mode.ts similarity index 73% rename from src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts rename to src/plugins/screenshot_mode/common/mode.ts index 850f70d2d002a..d4768ebff8308 100644 --- a/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts +++ b/src/plugins/screenshot_mode/common/mode.ts @@ -12,6 +12,12 @@ export const KBN_SCREENSHOT_MODE_ENABLED_KEY = '__KBN_SCREENSHOT_MODE_ENABLED_KEY__'; +declare global { + interface Window { + [KBN_SCREENSHOT_MODE_ENABLED_KEY]?: boolean; + } +} + /** * This function is responsible for detecting whether we are currently in screenshot mode. * @@ -21,7 +27,7 @@ export const KBN_SCREENSHOT_MODE_ENABLED_KEY = '__KBN_SCREENSHOT_MODE_ENABLED_KE */ export const getScreenshotMode = (): boolean => { return ( - (window as unknown as Record)[KBN_SCREENSHOT_MODE_ENABLED_KEY] === true || + window[KBN_SCREENSHOT_MODE_ENABLED_KEY] === true || window.localStorage.getItem(KBN_SCREENSHOT_MODE_ENABLED_KEY) === 'true' ); }; @@ -61,31 +67,3 @@ export const setScreenshotModeDisabled = () => { } ); }; - -/** @deprecated */ -export const KBN_SCREENSHOT_MODE_LAYOUT_KEY = '__KBN_SCREENSHOT_MODE_LAYOUT_KEY__'; - -/** @deprecated */ -export type Layout = 'canvas' | 'preserve_layout' | 'print'; - -/** @deprecated */ -export const setScreenshotLayout = (value: Layout) => { - Object.defineProperty( - window, - '__KBN_SCREENSHOT_MODE_LAYOUT_KEY__', // Literal value to prevent adding an external reference - { - enumerable: true, - writable: true, - configurable: false, - value, - } - ); -}; - -/** @deprecated */ -export const getScreenshotLayout = (): undefined | Layout => { - return ( - (window as unknown as Record)[KBN_SCREENSHOT_MODE_LAYOUT_KEY] || - (window.localStorage.getItem(KBN_SCREENSHOT_MODE_LAYOUT_KEY) as Layout) - ); -}; diff --git a/src/plugins/screenshot_mode/public/mocks.ts b/src/plugins/screenshot_mode/public/mocks.ts index d7e69e9d89211..86ef4c3cf1a42 100644 --- a/src/plugins/screenshot_mode/public/mocks.ts +++ b/src/plugins/screenshot_mode/public/mocks.ts @@ -11,11 +11,11 @@ import type { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './typ export const screenshotModePluginMock = { createSetupContract: (): DeeplyMockedKeys => ({ - getScreenshotLayout: jest.fn(), + getScreenshotContext: jest.fn(), isScreenshotMode: jest.fn(() => false), }), createStartContract: (): DeeplyMockedKeys => ({ - getScreenshotLayout: jest.fn(), + getScreenshotContext: jest.fn(), isScreenshotMode: jest.fn(() => false), }), }; diff --git a/src/plugins/screenshot_mode/public/plugin.ts b/src/plugins/screenshot_mode/public/plugin.ts index bb34fe84e2c39..2df8585cd6817 100644 --- a/src/plugins/screenshot_mode/public/plugin.ts +++ b/src/plugins/screenshot_mode/public/plugin.ts @@ -6,16 +6,14 @@ * Side Public License, v 1. */ -import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; - -import { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types'; - -import { getScreenshotMode, getScreenshotLayout } from '../common'; +import type { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { getScreenshotContext, getScreenshotMode } from '../common'; +import type { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types'; export class ScreenshotModePlugin implements Plugin { private publicContract = Object.freeze({ + getScreenshotContext, isScreenshotMode: () => getScreenshotMode() === true, - getScreenshotLayout, }); public setup(core: CoreSetup): ScreenshotModePluginSetup { diff --git a/src/plugins/screenshot_mode/public/types.ts b/src/plugins/screenshot_mode/public/types.ts index d1603cbceb26f..9de538bc9f8fb 100644 --- a/src/plugins/screenshot_mode/public/types.ts +++ b/src/plugins/screenshot_mode/public/types.ts @@ -6,18 +6,20 @@ * Side Public License, v 1. */ -import type { Layout } from '../common'; +export interface ScreenshotModePluginSetup { + /** + * Retrieves a value from the screenshotting context. + * @param key Context key to get. + * @param defaultValue Value to return if the key is not found. + * @return The value stored in the screenshotting context. + */ + getScreenshotContext(key: string, defaultValue?: T): T | undefined; -export interface IScreenshotModeService { /** * Returns a boolean indicating whether the current user agent (browser) would like to view UI optimized for * screenshots or printing. */ - isScreenshotMode: () => boolean; - - /** @deprecated */ - getScreenshotLayout: () => undefined | Layout; + isScreenshotMode(): boolean; } -export type ScreenshotModePluginSetup = IScreenshotModeService; -export type ScreenshotModePluginStart = IScreenshotModeService; +export type ScreenshotModePluginStart = ScreenshotModePluginSetup; diff --git a/src/plugins/screenshot_mode/server/is_screenshot_mode.ts b/src/plugins/screenshot_mode/server/is_screenshot_mode.ts index 79787bcd1fb50..8b8819f462770 100644 --- a/src/plugins/screenshot_mode/server/is_screenshot_mode.ts +++ b/src/plugins/screenshot_mode/server/is_screenshot_mode.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { KibanaRequest } from 'src/core/server'; +import type { KibanaRequest } from 'src/core/server'; import { KBN_SCREENSHOT_MODE_HEADER } from '../common'; export const isScreenshotMode = (request: KibanaRequest): boolean => { diff --git a/src/plugins/screenshot_mode/server/plugin.ts b/src/plugins/screenshot_mode/server/plugin.ts index b885ff97bf262..ff150b2f1a934 100644 --- a/src/plugins/screenshot_mode/server/plugin.ts +++ b/src/plugins/screenshot_mode/server/plugin.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { Plugin, CoreSetup } from '../../../core/server'; -import { +import type { Plugin, CoreSetup } from 'src/core/server'; +import type { ScreenshotModeRequestHandlerContext, ScreenshotModePluginSetup, ScreenshotModePluginStart, @@ -30,11 +30,11 @@ export class ScreenshotModePlugin // We use "require" here to ensure the import does not have external references due to code bundling that // commonly happens during transpiling. External references would be missing in the environment puppeteer creates. // eslint-disable-next-line @typescript-eslint/no-var-requires - const { setScreenshotModeEnabled, setScreenshotLayout } = require('../common'); + const { setScreenshotContext, setScreenshotModeEnabled } = require('../common'); return { + setScreenshotContext, setScreenshotModeEnabled, - setScreenshotLayout, isScreenshotMode, }; } diff --git a/src/plugins/screenshot_mode/server/types.ts b/src/plugins/screenshot_mode/server/types.ts index 1b9f3868f0966..5b9e50657674a 100644 --- a/src/plugins/screenshot_mode/server/types.ts +++ b/src/plugins/screenshot_mode/server/types.ts @@ -7,30 +7,29 @@ */ import type { RequestHandlerContext, KibanaRequest } from 'src/core/server'; -import type { Layout } from '../common'; -/** - * Any context that requires access to the screenshot mode flag but does not have access - * to request context {@link ScreenshotModeRequestHandlerContext}, for instance if they are pre-context, - * can use this function to check whether the request originates from a client that is in screenshot mode. - */ -type IsScreenshotMode = (request: KibanaRequest) => boolean; +export interface ScreenshotModePluginStart { + /** + * Any context that requires access to the screenshot mode flag but does not have access + * to request context {@link ScreenshotModeRequestHandlerContext}, for instance if they are pre-context, + * can use this function to check whether the request originates from a client that is in screenshot mode. + */ + isScreenshotMode(request: KibanaRequest): boolean; +} -export interface ScreenshotModePluginSetup { - isScreenshotMode: IsScreenshotMode; +export interface ScreenshotModePluginSetup extends ScreenshotModePluginStart { + /** + * Stores a value in the screenshotting context. + * @param key Context key to set. + * @param value Value to set. + */ + setScreenshotContext(key: string, value: T): void; /** * Set the current environment to screenshot mode. Intended to run in a browser-environment, before any other scripts * on the page have run to ensure that screenshot mode is detected as early as possible. */ - setScreenshotModeEnabled: () => void; - - /** @deprecated */ - setScreenshotLayout: (value: Layout) => void; -} - -export interface ScreenshotModePluginStart { - isScreenshotMode: IsScreenshotMode; + setScreenshotModeEnabled(): void; } export interface ScreenshotModeRequestHandlerContext extends RequestHandlerContext { diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index fdcb506acedf7..b4ecd60f8ac59 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index fdcb506acedf7..b4ecd60f8ac59 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index 152ad083e72a8..ec04c29dfdcd7 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_empty_data.json b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json index 241ee3ec029cc..a866540900e95 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_empty_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 0c41486ecb27a..dbb63cd00e070 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index 8aa42180c755b..b43986250d4c7 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index e2f11f055ebb4..d65dd9dc4a775 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index fdcb506acedf7..b4ecd60f8ac59 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index fdcb506acedf7..b4ecd60f8ac59 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/x-pack/examples/reporting_example/kibana.json b/x-pack/examples/reporting_example/kibana.json index 489b2bcd9f506..21329eba30a68 100644 --- a/x-pack/examples/reporting_example/kibana.json +++ b/x-pack/examples/reporting_example/kibana.json @@ -17,6 +17,5 @@ "navigation", "screenshotMode", "share" - ], - "requiredBundles": ["screenshotting"] + ] } diff --git a/x-pack/examples/reporting_example/public/containers/main.tsx b/x-pack/examples/reporting_example/public/containers/main.tsx index 2a72f0ee71661..6242073485220 100644 --- a/x-pack/examples/reporting_example/public/containers/main.tsx +++ b/x-pack/examples/reporting_example/public/containers/main.tsx @@ -40,7 +40,7 @@ import type { JobParamsPNGV2, } from '../../../../plugins/reporting/common/types'; import type { ReportingStart } from '../../../../plugins/reporting/public'; -import { LayoutTypes } from '../../../../plugins/screenshotting/public'; +import { LayoutTypes } from '../../../../plugins/screenshotting/common'; import { REPORTING_EXAMPLE_LOCATOR_ID } from '../../common'; import { useApplicationContext } from '../application_context'; import { ROUTES } from '../constants'; diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 86e81d78f69fd..2895d7d376666 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -74,7 +74,7 @@ Arguments: | userCanCrud | `boolean;` user permissions to crud | | owner | `string[];` owner ids of the cases | | basePath | `string;` path to mount the Cases router on top of | -| useFetchAlertData | `(alertIds: string[]) => [boolean, Record];` fetch alerts | +| useFetchAlertData | `(alertIds: string[]) => [boolean, Record];` fetch alerts | | disableAlerts? | `boolean` (default: false) flag to not show alerts information | | actionsNavigation? | CasesNavigation | | ruleDetailsNavigation? | CasesNavigation | diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 008d4b9245f63..f6bfb510cab81 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -253,3 +253,5 @@ export interface Ecs { } export type CaseActionConnector = ActionConnector; + +export type UseFetchAlertData = (alertIds: string[]) => [boolean, Record]; diff --git a/x-pack/plugins/cases/public/components/app/types.ts b/x-pack/plugins/cases/public/components/app/types.ts index ebe174c095fa7..1df4d54188ec0 100644 --- a/x-pack/plugins/cases/public/components/app/types.ts +++ b/x-pack/plugins/cases/public/components/app/types.ts @@ -6,7 +6,7 @@ */ import { MutableRefObject } from 'react'; -import { Ecs, CaseViewRefreshPropInterface } from '../../../common/ui/types'; +import { CaseViewRefreshPropInterface, UseFetchAlertData } from '../../../common/ui/types'; import { CasesNavigation } from '../links'; import { CasesTimelineIntegration } from '../timeline_context'; @@ -15,7 +15,7 @@ export interface CasesRoutesProps { actionsNavigation?: CasesNavigation; ruleDetailsNavigation?: CasesNavigation; showAlertDetails?: (alertId: string, index: string) => void; - useFetchAlertData: (alertIds: string[]) => [boolean, Record]; + useFetchAlertData: UseFetchAlertData; /** * A React `Ref` that Exposes data refresh callbacks. * **NOTE**: Do not hold on to the `.current` object, as it could become stale diff --git a/x-pack/plugins/cases/public/components/case_view/types.ts b/x-pack/plugins/cases/public/components/case_view/types.ts index 69d05918e182f..3d436a7db3186 100644 --- a/x-pack/plugins/cases/public/components/case_view/types.ts +++ b/x-pack/plugins/cases/public/components/case_view/types.ts @@ -7,15 +7,16 @@ import { MutableRefObject } from 'react'; import { CasesTimelineIntegration } from '../timeline_context'; import { CasesNavigation } from '../links'; -import { CaseViewRefreshPropInterface, Ecs, Case } from '../../../common'; +import { CaseViewRefreshPropInterface, Case } from '../../../common'; import { UseGetCase } from '../../containers/use_get_case'; +import { UseFetchAlertData } from '../../../common/ui'; export interface CaseViewBaseProps { onComponentInitialized?: () => void; actionsNavigation?: CasesNavigation; ruleDetailsNavigation?: CasesNavigation; showAlertDetails?: (alertId: string, index: string) => void; - useFetchAlertData: (alertIds: string[]) => [boolean, Record]; + useFetchAlertData: UseFetchAlertData; /** * A React `Ref` that Exposes data refresh callbacks. * **NOTE**: Do not hold on to the `.current` object, as it could become stale diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/alert.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/alert.tsx index f0533f62de234..95e996498554a 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/alert.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/alert.tsx @@ -18,7 +18,6 @@ import { UserActionUsernameWithAvatar } from '../avatar_username'; import { AlertCommentEvent } from './alert_event'; import { UserActionCopyLink } from '../copy_link'; import { UserActionShowAlert } from './show_alert'; -import { Ecs } from '../../../containers/types'; type BuilderArgs = Pick< UserActionBuilderArgs, @@ -49,7 +48,7 @@ export const createAlertAttachmentUserActionBuilder = ({ return []; } - const alertField: Ecs | undefined = alertData[alertId]; + const alertField: unknown | undefined = alertData[alertId]; const ruleId = getRuleId(comment, alertField); const ruleName = getRuleName(comment, alertField); @@ -101,7 +100,7 @@ const getFirstItem = (items?: string | string[] | null): string | null => { return Array.isArray(items) ? items[0] : items ?? null; }; -export const getRuleId = (comment: BuilderArgs['comment'], alertData?: Ecs): string | null => +export const getRuleId = (comment: BuilderArgs['comment'], alertData?: unknown): string | null => getRuleField({ commentRuleField: comment?.rule?.id, alertData, @@ -109,7 +108,7 @@ export const getRuleId = (comment: BuilderArgs['comment'], alertData?: Ecs): str kibanaAlertFieldPath: ALERT_RULE_UUID, }); -export const getRuleName = (comment: BuilderArgs['comment'], alertData?: Ecs): string | null => +export const getRuleName = (comment: BuilderArgs['comment'], alertData?: unknown): string | null => getRuleField({ commentRuleField: comment?.rule?.name, alertData, @@ -124,7 +123,7 @@ const getRuleField = ({ kibanaAlertFieldPath, }: { commentRuleField: string | string[] | null | undefined; - alertData: Ecs | undefined; + alertData: unknown | undefined; signalRuleFieldPath: string; kibanaAlertFieldPath: string; }): string | null => { diff --git a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx index 2426e74f3e7b6..d05f378cd205a 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx @@ -22,7 +22,6 @@ import { } from '../../containers/mock'; import { UserActions } from '.'; import { TestProviders } from '../../common/mock'; -import { Ecs } from '../../../common/ui/types'; import { Actions } from '../../../common/api'; const fetchUserActions = jest.fn(); @@ -46,7 +45,7 @@ const defaultProps = { statusActionButton: null, updateCase, userCanCrud: true, - useFetchAlertData: (): [boolean, Record] => [ + useFetchAlertData: (): [boolean, Record] => [ false, { 'some-id': { _id: 'some-id' } }, ], diff --git a/x-pack/plugins/cases/public/components/user_actions/index.tsx b/x-pack/plugins/cases/public/components/user_actions/index.tsx index 33394017a4698..d980ef029e447 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.tsx @@ -98,10 +98,13 @@ export const UserActions = React.memo( const [initLoading, setInitLoading] = useState(true); const currentUser = useCurrentUser(); - const [loadingAlertData, manualAlertsData] = useFetchAlertData( - getManualAlertIdsWithNoRuleId(caseData.comments) + const alertIdsWithoutRuleInfo = useMemo( + () => getManualAlertIdsWithNoRuleId(caseData.comments), + [caseData.comments] ); + const [loadingAlertData, manualAlertsData] = useFetchAlertData(alertIdsWithoutRuleInfo); + const { loadingCommentIds, commentRefs, diff --git a/x-pack/plugins/cases/public/components/user_actions/types.ts b/x-pack/plugins/cases/public/components/user_actions/types.ts index dece59ec1eb42..37beaca4dc6b6 100644 --- a/x-pack/plugins/cases/public/components/user_actions/types.ts +++ b/x-pack/plugins/cases/public/components/user_actions/types.ts @@ -8,7 +8,7 @@ import { EuiCommentProps } from '@elastic/eui'; import { SnakeToCamelCase } from '../../../common/types'; import { ActionTypes, UserActionWithResponse } from '../../../common/api'; -import { Case, CaseUserActions, Ecs, Comment } from '../../containers/types'; +import { Case, CaseUserActions, Comment, UseFetchAlertData } from '../../containers/types'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { AddCommentRefObject } from '../add_comment'; import { UserActionMarkdownRefObject } from './markdown_form'; @@ -30,7 +30,7 @@ export interface UserActionTreeProps { onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void; statusActionButton: JSX.Element | null; updateCase: (newCase: Case) => void; - useFetchAlertData: (alertIds: string[]) => [boolean, Record]; + useFetchAlertData: UseFetchAlertData; userCanCrud: boolean; } @@ -51,7 +51,7 @@ export interface UserActionBuilderArgs { selectedOutlineCommentId: string; loadingCommentIds: string[]; loadingAlertData: boolean; - alertData: Record; + alertData: Record; handleOutlineComment: (id: string) => void; handleManageMarkdownEditId: (id: string) => void; handleSaveComment: ({ id, version }: { id: string; version: string }, content: string) => void; diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 8d2582d9c5857..5881b7b7633be 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -59,11 +59,13 @@ export interface PluginStartContract { export class CasePlugin { private readonly log: Logger; + private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; private clientFactory: CasesClientFactory; private securityPluginSetup?: SecurityPluginSetup; private lensEmbeddableFactory?: LensServerPluginSetup['lensEmbeddableFactory']; constructor(private readonly initializerContext: PluginInitializerContext) { + this.kibanaVersion = initializerContext.env.packageInfo.version; this.log = this.initializerContext.logger.get(); this.clientFactory = new CasesClientFactory(this.log); } @@ -101,6 +103,7 @@ export class CasePlugin { initCaseApi({ logger: this.log, router, + kibanaVersion: this.kibanaVersion, }); } diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index f8b85313482f4..c8558d09e5c5f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -8,10 +8,10 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; +import { getWarningHeader, logDeprecatedEndpoint, wrapError } from '../utils'; import { CASE_DETAILS_URL } from '../../../../common/constants'; -export function initGetCaseApi({ router, logger }: RouteDeps) { +export function initGetCaseApi({ router, logger, kibanaVersion }: RouteDeps) { router.get( { path: CASE_DETAILS_URL, @@ -20,16 +20,35 @@ export function initGetCaseApi({ router, logger }: RouteDeps) { case_id: schema.string(), }), query: schema.object({ + /** + * @deprecated since version 8.1.0 + */ includeComments: schema.boolean({ defaultValue: true }), }), }, }, async (context, request, response) => { try { + const isIncludeCommentsParamProvidedByTheUser = + request.url.searchParams.has('includeComments'); + + if (isIncludeCommentsParamProvidedByTheUser) { + logDeprecatedEndpoint( + logger, + request.headers, + `The query parameter 'includeComments' of the get case API '${CASE_DETAILS_URL}' is deprecated` + ); + } + const casesClient = await context.cases.getCasesClient(); const id = request.params.case_id; return response.ok({ + ...(isIncludeCommentsParamProvidedByTheUser && { + headers: { + ...getWarningHeader(kibanaVersion, 'Deprecated query parameter includeComments'), + }, + }), body: await casesClient.cases.get({ id, includeComments: request.query.includeComments, diff --git a/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts index d7f95d503c394..e94b19cdd9a1c 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts @@ -8,10 +8,13 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; +import { wrapError, getWarningHeader, logDeprecatedEndpoint } from '../utils'; import { CASE_COMMENTS_URL } from '../../../../common/constants'; -export function initGetAllCommentsApi({ router, logger }: RouteDeps) { +/** + * @deprecated since version 8.1.0 + */ +export function initGetAllCommentsApi({ router, logger, kibanaVersion }: RouteDeps) { router.get( { path: CASE_COMMENTS_URL, @@ -23,9 +26,18 @@ export function initGetAllCommentsApi({ router, logger }: RouteDeps) { }, async (context, request, response) => { try { + logDeprecatedEndpoint( + logger, + request.headers, + `The get all cases comments API '${CASE_COMMENTS_URL}' is deprecated.` + ); + const client = await context.cases.getCasesClient(); return response.ok({ + headers: { + ...getWarningHeader(kibanaVersion), + }, body: await client.attachments.getAll({ caseID: request.params.case_id, }), diff --git a/x-pack/plugins/cases/server/routes/api/stats/get_status.ts b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts index ffbc101a75dc4..90044c9516b3a 100644 --- a/x-pack/plugins/cases/server/routes/api/stats/get_status.ts +++ b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts @@ -6,12 +6,15 @@ */ import { RouteDeps } from '../types'; -import { escapeHatch, wrapError } from '../utils'; +import { escapeHatch, wrapError, getWarningHeader, logDeprecatedEndpoint } from '../utils'; import { CasesStatusRequest } from '../../../../common/api'; import { CASE_STATUS_URL } from '../../../../common/constants'; -export function initGetCasesStatusApi({ router, logger }: RouteDeps) { +/** + * @deprecated since version 8.1.0 + */ +export function initGetCasesStatusApi({ router, logger, kibanaVersion }: RouteDeps) { router.get( { path: CASE_STATUS_URL, @@ -19,8 +22,17 @@ export function initGetCasesStatusApi({ router, logger }: RouteDeps) { }, async (context, request, response) => { try { + logDeprecatedEndpoint( + logger, + request.headers, + `The get cases status API '${CASE_STATUS_URL}' is deprecated.` + ); + const client = await context.cases.getCasesClient(); return response.ok({ + headers: { + ...getWarningHeader(kibanaVersion), + }, body: await client.metrics.getStatusTotalsByType(request.query as CasesStatusRequest), }); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/types.ts b/x-pack/plugins/cases/server/routes/api/types.ts index 9211aee5606a6..e3aa6e0e970fa 100644 --- a/x-pack/plugins/cases/server/routes/api/types.ts +++ b/x-pack/plugins/cases/server/routes/api/types.ts @@ -5,13 +5,14 @@ * 2.0. */ -import type { Logger } from 'kibana/server'; +import type { Logger, PluginInitializerContext } from 'kibana/server'; import type { CasesRouter } from '../../types'; export interface RouteDeps { router: CasesRouter; logger: Logger; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; } export interface TotalCommentByCase { diff --git a/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts index cf258c284568b..2e38ac8b4ebc7 100644 --- a/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts @@ -8,10 +8,13 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; +import { getWarningHeader, logDeprecatedEndpoint, wrapError } from '../utils'; import { CASE_USER_ACTIONS_URL } from '../../../../common/constants'; -export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { +/** + * @deprecated since version 8.1.0 + */ +export function initGetAllCaseUserActionsApi({ router, logger, kibanaVersion }: RouteDeps) { router.get( { path: CASE_USER_ACTIONS_URL, @@ -27,10 +30,19 @@ export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); } + logDeprecatedEndpoint( + logger, + request.headers, + `The get all cases user actions API '${CASE_USER_ACTIONS_URL}' is deprecated.` + ); + const casesClient = await context.cases.getCasesClient(); const caseId = request.params.case_id; return response.ok({ + headers: { + ...getWarningHeader(kibanaVersion), + }, body: await casesClient.userActions.getAll({ caseId }), }); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/utils.test.ts b/x-pack/plugins/cases/server/routes/api/utils.test.ts index c2cff04f56a49..2614536e3242b 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.test.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.test.ts @@ -6,8 +6,9 @@ */ import { isBoom, boomify } from '@hapi/boom'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { HTTPError } from '../../common/error'; -import { wrapError } from './utils'; +import { logDeprecatedEndpoint, wrapError } from './utils'; describe('Utils', () => { describe('wrapError', () => { @@ -55,4 +56,23 @@ describe('Utils', () => { expect(res.headers).toEqual({}); }); }); + + describe('logDeprecatedEndpoint', () => { + const logger = loggingSystemMock.createLogger(); + const kibanaHeader = { 'kbn-version': '8.1.0', referer: 'test' }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does NOT log when the request is from the kibana client', () => { + logDeprecatedEndpoint(logger, kibanaHeader, 'test'); + expect(logger.warn).not.toHaveBeenCalledWith('test'); + }); + + it('does log when the request is NOT from the kibana client', () => { + logDeprecatedEndpoint(logger, {}, 'test'); + expect(logger.warn).toHaveBeenCalledWith('test'); + }); + }); }); diff --git a/x-pack/plugins/cases/server/routes/api/utils.ts b/x-pack/plugins/cases/server/routes/api/utils.ts index a09fd4cc9c746..532a316e9a7b8 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.ts @@ -6,9 +6,8 @@ */ import { Boom, boomify, isBoom } from '@hapi/boom'; - import { schema } from '@kbn/config-schema'; -import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; +import type { CustomHttpResponseOptions, ResponseError, Headers, Logger } from 'kibana/server'; import { CaseError, isCaseError, HTTPError, isHTTPError } from '../../common/error'; /** @@ -35,3 +34,32 @@ export function wrapError( } export const escapeHatch = schema.object({}, { unknowns: 'allow' }); + +/** + * Creates a warning header with a message formatted according to RFC7234. + * We follow the same formatting as Elasticsearch + * https://github.com/elastic/elasticsearch/blob/5baabff6670a8ed49297488ca8cac8ec12a2078d/server/src/main/java/org/elasticsearch/common/logging/HeaderWarning.java#L55 + */ +export const getWarningHeader = ( + kibanaVersion: string, + msg: string | undefined = 'Deprecated endpoint' +): { warning: string } => ({ + warning: `299 Kibana-${kibanaVersion} "${msg}"`, +}); + +/** + * Taken from + * https://github.com/elastic/kibana/blob/ec30f2aeeb10fb64b507935e558832d3ef5abfaa/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts#L113-L118 + */ + +const getIsKibanaRequest = (headers?: Headers) => { + // The presence of these two request headers gives us a good indication that this is a first-party request from the Kibana client. + // We can't be 100% certain, but this is a reasonable attempt. + return headers && headers['kbn-version'] && headers.referer; +}; + +export const logDeprecatedEndpoint = (logger: Logger, headers: Headers, msg: string) => { + if (!getIsKibanaRequest(headers)) { + logger.warn(msg); + } +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx index dc931f835b043..6dae7920fa692 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx @@ -13,9 +13,10 @@ import { groupBy } from 'lodash'; import type { ResolvedSimpleSavedObject } from 'src/core/public'; +import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { Loading, Error, ExtensionWrapper } from '../../../../../components'; -import type { PackageInfo } from '../../../../../types'; +import type { PackageInfo, StartPlugins } from '../../../../../types'; import { InstallStatus } from '../../../../../types'; import { @@ -36,8 +37,8 @@ interface AssetsPanelProps { export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { const { name, version } = packageInfo; + const { spaces } = useKibana().services; const pkgkey = `${name}-${version}`; - const { savedObjects: { client: savedObjectsClient }, } = useStartServices(); @@ -47,6 +48,8 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(packageInfo.name); + // assume assets are installed in this space until we find otherwise + const [assetsInstalledInCurrentSpace, setAssetsInstalledInCurrentSpace] = useState(true); const [assetSavedObjects, setAssetsSavedObjects] = useState(); const [fetchError, setFetchError] = useState(); const [isLoading, setIsLoading] = useState(true); @@ -54,6 +57,17 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { useEffect(() => { const fetchAssetSavedObjects = async () => { if ('savedObject' in packageInfo) { + if (spaces) { + const { id: spaceId } = await spaces.getActiveSpace(); + const assetInstallSpaceId = packageInfo.savedObject.attributes.installed_kibana_space_id; + // if assets are installed in a different space no need to attempt to load them. + if (assetInstallSpaceId && assetInstallSpaceId !== spaceId) { + setAssetsInstalledInCurrentSpace(false); + setIsLoading(false); + return; + } + } + const { savedObject: { attributes: packageAttributes }, } = packageInfo; @@ -114,7 +128,7 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { } }; fetchAssetSavedObjects(); - }, [savedObjectsClient, packageInfo]); + }, [savedObjectsClient, packageInfo, spaces]); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab @@ -137,9 +151,20 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { error={fetchError} /> ); + } else if (!assetsInstalledInCurrentSpace) { + content = ( + +

+ +

+
+ ); } else if (assetSavedObjects === undefined || assetSavedObjects.length === 0) { if (customAssetsExtension) { - // If a UI extension for custom asset entries is defined, render the custom component here depisite + // If a UI extension for custom asset entries is defined, render the custom component here despite // there being no saved objects found content = ( diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts index 79cfb7ff2b2d1..acb9b198fdcba 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts @@ -13,6 +13,7 @@ jest.mock('../../hooks/use_request', () => { sendGetFleetStatus: jest.fn(), sendGetOneAgentPolicy: jest.fn(), useGetAgents: jest.fn(), + useGetAgentPolicies: jest.fn(), }; }); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx index 617bd95e18a6d..b46996ef164bd 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx @@ -21,6 +21,7 @@ import { sendGetFleetStatus, sendGetOneAgentPolicy, useGetAgents, + useGetAgentPolicies, } from '../../hooks/use_request'; import { FleetStatusProvider, ConfigContext } from '../../hooks'; @@ -101,6 +102,10 @@ describe('', () => { data: { items: [{ policy_id: 'fleet-server-policy' }] }, }); + (useGetAgentPolicies as jest.Mock).mockReturnValue?.({ + data: { items: [{ id: 'fleet-server-policy' }] }, + }); + await act(async () => { testBed = await setup({ agentPolicies: [{ id: 'fleet-server-policy' } as AgentPolicy], @@ -143,6 +148,25 @@ describe('', () => { }); }); + describe('with a specific policy when no agentPolicies set', () => { + beforeEach(async () => { + jest.clearAllMocks(); + await act(async () => { + testBed = await setup({ + agentPolicy: testAgentPolicy, + onClose: jest.fn(), + }); + testBed.component.update(); + }); + }); + + it('should not show fleet server instructions', () => { + const { exists } = testBed; + expect(exists('agentEnrollmentFlyout')).toBe(true); + expect(AgentEnrollmentKeySelectionStep).toHaveBeenCalled(); + }); + }); + // Skipped due to implementation details in the step components. See https://github.com/elastic/kibana/issues/103894 describe.skip('"View data" extension point', () => { it('shows the "View data" step when UI extension is provided', async () => { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index 6d65476e3641f..d3294692c9e55 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -11,7 +11,13 @@ import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/st import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useGetOneEnrollmentAPIKey, useLink, useFleetStatus, useGetAgents } from '../../hooks'; +import { + useGetOneEnrollmentAPIKey, + useLink, + useFleetStatus, + useGetAgents, + useGetAgentPolicies, +} from '../../hooks'; import { ManualInstructions } from '../../components/enrollment_instructions'; import { @@ -81,14 +87,24 @@ export const ManagedInstructions = React.memo( showInactive: false, }); + const { data: agentPoliciesData, isLoading: isLoadingAgentPolicies } = useGetAgentPolicies({ + page: 1, + perPage: 1000, + full: true, + }); + const fleetServers = useMemo(() => { - const fleetServerAgentPolicies: string[] = (agentPolicies ?? []) + let policies = agentPolicies; + if (!agentPolicies && !isLoadingAgentPolicies) { + policies = agentPoliciesData?.items; + } + const fleetServerAgentPolicies: string[] = (policies ?? []) .filter((pol) => policyHasFleetServer(pol)) .map((pol) => pol.id); return (agents?.items ?? []).filter((agent) => fleetServerAgentPolicies.includes(agent.policy_id ?? '') ); - }, [agents, agentPolicies]); + }, [agents, agentPolicies, agentPoliciesData, isLoadingAgentPolicies]); const fleetServerSteps = useMemo(() => { const { diff --git a/x-pack/plugins/fleet/public/types/index.ts b/x-pack/plugins/fleet/public/types/index.ts index ead44a798cfc7..6fbe645c9bea3 100644 --- a/x-pack/plugins/fleet/public/types/index.ts +++ b/x-pack/plugins/fleet/public/types/index.ts @@ -121,3 +121,4 @@ export { entries, ElasticsearchAssetType, KibanaAssetType, InstallStatus } from export * from './intra_app_route_state'; export * from './ui_extensions'; export * from './in_memory_package_policy'; +export * from './start_plugins'; diff --git a/x-pack/plugins/fleet/public/types/start_plugins.ts b/x-pack/plugins/fleet/public/types/start_plugins.ts new file mode 100644 index 0000000000000..8fcad821f2ccb --- /dev/null +++ b/x-pack/plugins/fleet/public/types/start_plugins.ts @@ -0,0 +1,12 @@ +/* + * 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 { SpacesPluginStart } from '../../../spaces/public'; + +export interface StartPlugins { + spaces?: SpacesPluginStart; +} diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts index fd92c14e33808..f703b1b5f419b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts @@ -10,9 +10,18 @@ import type { PaletteRegistry } from 'src/plugins/charts/public'; import type { Operation, DatasourcePublicAPI } from '../types'; import { DEFAULT_PERCENT_DECIMALS, EMPTY_SIZE_RATIOS } from './constants'; import { shouldShowValuesInLegend } from './render_helpers'; -import type { PieVisualizationState } from '../../common/expressions'; +import type { PieLayerState, PieVisualizationState } from '../../common/expressions'; import { getDefaultVisualValuesForLayer } from '../shared_components/datasource_default_values'; +export const getSortedGroups = (datasource: DatasourcePublicAPI, layer: PieLayerState) => { + const originalOrder = datasource + .getTableSpec() + .map(({ columnId }: { columnId: string }) => columnId) + .filter((columnId: string) => layer.groups.includes(columnId)); + // When we add a column it could be empty, and therefore have no order + return Array.from(new Set(originalOrder.concat(layer.groups))); +}; + export function toExpression( state: PieVisualizationState, datasourceLayers: Record, @@ -33,14 +42,15 @@ function expressionHelper( ): Ast | null { const layer = state.layers[0]; const datasource = datasourceLayers[layer.layerId]; - const operations = layer.groups + const groups = getSortedGroups(datasource, layer); + + const operations = groups .map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) })) .filter((o): o is { columnId: string; operation: Operation } => !!o.operation); if (!layer.metric || !operations.length) { return null; } - return { type: 'expression', chain: [ diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index eb154abe6d14d..5bf2ae8822583 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -19,7 +19,7 @@ import type { AccessorConfig, VisualizationDimensionGroupConfig, } from '../types'; -import { toExpression, toPreviewExpression } from './to_expression'; +import { getSortedGroups, toExpression, toPreviewExpression } from './to_expression'; import type { PieLayerState, PieVisualizationState } from '../../common/expressions'; import { layerTypes } from '../../common'; import { suggestions } from './suggestions'; @@ -126,14 +126,11 @@ export const getPieVisualization = ({ } const datasource = frame.datasourceLayers[layer.layerId]; - const originalOrder = datasource - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => columnId !== layer.metric); + const originalOrder = getSortedGroups(datasource, layer); // When we add a column it could be empty, and therefore have no order - const sortedColumns: AccessorConfig[] = Array.from( - new Set(originalOrder.concat(layer.groups)) - ).map((accessor) => ({ columnId: accessor })); + const sortedColumns: AccessorConfig[] = originalOrder.map((accessor) => ({ + columnId: accessor, + })); if (sortedColumns.length) { applyPaletteToColumnConfig(sortedColumns, state, paletteService); @@ -197,7 +194,7 @@ export const getPieVisualization = ({ return l; } if (groupId === 'groups') { - return { ...l, groups: [...l.groups, columnId] }; + return { ...l, groups: [...l.groups.filter((group) => group !== columnId), columnId] }; } return { ...l, metric: columnId }; }), diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx index 85fae39a05910..b83410a4eef04 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx @@ -23,7 +23,7 @@ export const newVectorLayerWizardConfig: LayerWizard = { }), disabledReason: i18n.translate('xpack.maps.newVectorLayerWizard.disabledDesc', { defaultMessage: - 'Unable to create index, you are missing the Kibana privilege "Index Pattern Management".', + 'Unable to create index, you are missing the Kibana privilege "Data View Management".', }), getIsDisabled: async () => { const hasImportPermission = await getFileUpload().hasImportPermission({ diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts index 79e2cff4da302..4cc8361be08cc 100644 --- a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts +++ b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts @@ -11,10 +11,10 @@ import type { DashboardAppLocator } from '../../../../../../src/plugins/dashboar describe('DashboardService', () => { const mockSavedObjectClient = savedObjectsServiceMock.createStartContract().client; - const dashboardUrlGenerator = { + const dashboardLocator = { getUrl: jest.fn(), } as unknown as DashboardAppLocator; - const dashboardService = dashboardServiceProvider(mockSavedObjectClient, dashboardUrlGenerator); + const dashboardService = dashboardServiceProvider(mockSavedObjectClient, dashboardLocator); test('should fetch dashboard', () => { // act @@ -30,7 +30,7 @@ describe('DashboardService', () => { test('should generate edit url to the dashboard', () => { dashboardService.getDashboardEditUrl('test-id'); - expect(dashboardUrlGenerator.getUrl).toHaveBeenCalledWith({ + expect(dashboardLocator.getUrl).toHaveBeenCalledWith({ dashboardId: 'test-id', useHash: false, viewMode: 'edit', diff --git a/x-pack/plugins/observability/public/pages/cases/cases.tsx b/x-pack/plugins/observability/public/pages/cases/cases.tsx index 19eb16a3bd52b..4b9810421ba5f 100644 --- a/x-pack/plugins/observability/public/pages/cases/cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/cases.tsx @@ -8,10 +8,11 @@ import React, { Suspense, useCallback, useState } from 'react'; import { useKibana } from '../../utils/kibana_react'; -import { useFetchAlertData, useFetchAlertDetail } from './helpers'; import { CASES_OWNER, CASES_PATH } from './constants'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { LazyAlertsFlyout } from '../..'; +import { useFetchAlertDetail } from './use_fetch_alert_detail'; +import { useFetchAlertData } from './use_fetch_alert_data'; interface CasesProps { userCanCrud: boolean; diff --git a/x-pack/plugins/observability/public/pages/cases/helpers.ts b/x-pack/plugins/observability/public/pages/cases/helpers.ts deleted file mode 100644 index f4bc5af7f604d..0000000000000 --- a/x-pack/plugins/observability/public/pages/cases/helpers.ts +++ /dev/null @@ -1,53 +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 { useEffect, useState } from 'react'; -import { isEmpty } from 'lodash'; -import { usePluginContext } from '../../hooks/use_plugin_context'; -import { TopAlert, parseAlert } from '../../pages/alerts/'; -import { useKibana } from '../../utils/kibana_react'; -import { Ecs } from '../../../../cases/common'; - -// no alerts in observability so far -// dummy hook for now as hooks cannot be called conditionally -export const useFetchAlertData = (): [boolean, Record] => [false, {}]; - -export const useFetchAlertDetail = (alertId: string): [boolean, TopAlert | null] => { - const { http } = useKibana().services; - const [loading, setLoading] = useState(false); - const { observabilityRuleTypeRegistry } = usePluginContext(); - const [alert, setAlert] = useState(null); - - useEffect(() => { - const abortCtrl = new AbortController(); - const fetchData = async () => { - try { - setLoading(true); - const response = await http.get>('/internal/rac/alerts', { - query: { - id: alertId, - }, - }); - if (response) { - const parsedAlert = parseAlert(observabilityRuleTypeRegistry)(response); - setAlert(parsedAlert); - setLoading(false); - } - } catch (error) { - setAlert(null); - } - }; - - if (!isEmpty(alertId) && loading === false && alert === null) { - fetchData(); - } - return () => { - abortCtrl.abort(); - }; - }, [http, alertId, alert, loading, observabilityRuleTypeRegistry]); - - return [loading, alert]; -}; diff --git a/x-pack/plugins/observability/public/pages/cases/use_data_fetcher.ts b/x-pack/plugins/observability/public/pages/cases/use_data_fetcher.ts new file mode 100644 index 0000000000000..2ee585b4f81ad --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/use_data_fetcher.ts @@ -0,0 +1,65 @@ +/* + * 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 { useState, useMemo, useEffect } from 'react'; + +import { HttpSetup } from 'kibana/public'; +import { useKibana } from '../../utils/kibana_react'; + +type DataFetcher = (params: T, ctrl: AbortController, http: HttpSetup) => Promise; + +export const useDataFetcher = ({ + paramsForApiCall, + initialDataState, + executeApiCall, + shouldExecuteApiCall, +}: { + paramsForApiCall: ApiCallParams; + initialDataState: AlertDataType; + executeApiCall: DataFetcher; + shouldExecuteApiCall: (params: ApiCallParams) => boolean; +}) => { + const { http } = useKibana().services; + const [loading, setLoading] = useState(false); + const [data, setData] = useState(initialDataState); + + const { fetch, cancel } = useMemo(() => { + const abortController = new AbortController(); + let isCanceled = false; + + return { + fetch: async () => { + if (shouldExecuteApiCall(paramsForApiCall)) { + setLoading(true); + + const results = await executeApiCall(paramsForApiCall, abortController, http); + if (!isCanceled) { + setLoading(false); + setData(results); + } + } + }, + cancel: () => { + isCanceled = true; + abortController.abort(); + }, + }; + }, [executeApiCall, http, paramsForApiCall, shouldExecuteApiCall]); + + useEffect(() => { + fetch(); + + return () => { + cancel(); + }; + }, [fetch, cancel]); + + return { + loading, + data, + }; +}; diff --git a/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_data.test.ts b/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_data.test.ts new file mode 100644 index 0000000000000..815bafc45c97c --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_data.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { kibanaStartMock } from '../../utils/kibana_react.mock'; +import { useFetchAlertData } from './use_fetch_alert_data'; + +const mockUseKibanaReturnValue = kibanaStartMock.startContract(); + +jest.mock('../../utils/kibana_react', () => ({ + __esModule: true, + useKibana: jest.fn(() => mockUseKibanaReturnValue), +})); + +describe('useFetchAlertData', () => { + const testIds = ['123']; + + beforeEach(() => { + mockUseKibanaReturnValue.services.http.post.mockImplementation(async () => ({ + hits: { + hits: [ + { + _id: '123', + _index: 'index', + _source: { + testField: 'test', + }, + }, + ], + }, + })); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('initially is not loading and does not have data', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook]>( + () => useFetchAlertData(testIds) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual([false, {}]); + }); + }); + + it('returns no data when an error occurs', async () => { + mockUseKibanaReturnValue.services.http.post.mockImplementation(async () => { + throw new Error('an http error'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook]>( + () => useFetchAlertData(testIds) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual([false, {}]); + }); + }); + + it('retrieves the alert data', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook]>( + () => useFetchAlertData(testIds) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual([ + false, + { '123': { _id: '123', _index: 'index', testField: 'test' } }, + ]); + }); + }); + + it('does not populate the results when the request is canceled', async () => { + await act(async () => { + const { result, waitForNextUpdate, unmount } = renderHook< + string, + [boolean, Record] + >(() => useFetchAlertData(testIds)); + + await waitForNextUpdate(); + unmount(); + + expect(result.current).toEqual([false, {}]); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_data.ts b/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_data.ts new file mode 100644 index 0000000000000..1e47094be866c --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_data.ts @@ -0,0 +1,78 @@ +/* + * 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 { useCallback, useMemo } from 'react'; +import { isEmpty } from 'lodash'; + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { HttpSetup } from 'kibana/public'; +import { BASE_RAC_ALERTS_API_PATH } from '../../../../rule_registry/common/constants'; +import { useDataFetcher } from './use_data_fetcher'; + +export const useFetchAlertData = (alertIds: string[]): [boolean, Record] => { + const validIds = useMemo(() => getValidValues(alertIds), [alertIds]); + const shouldExecuteApiCall = useCallback((ids: string[]) => ids.length > 0, []); + + const { loading, data: alerts } = useDataFetcher | undefined>({ + paramsForApiCall: validIds, + initialDataState: undefined, + executeApiCall: fetchAlerts, + shouldExecuteApiCall, + }); + + return [loading, alerts ?? {}]; +}; + +const fetchAlerts = async ( + ids: string[], + abortCtrl: AbortController, + http: HttpSetup +): Promise | undefined> => { + try { + const response = await http.post>>( + `${BASE_RAC_ALERTS_API_PATH}/find`, + { + body: JSON.stringify({ + query: { + ids: { + values: ids, + }, + }, + track_total_hits: false, + size: 10000, + }), + signal: abortCtrl.signal, + } + ); + + if (response) { + return getAlertsGroupedById(response); + } + } catch (error) { + // ignore the failure + } +}; + +const getAlertsGroupedById = ( + data: estypes.SearchResponse> +): Record => { + return data.hits.hits.reduce( + (acc, { _id, _index, _source }) => ({ + ...acc, + [_id]: { + _id, + _index, + ..._source, + }, + }), + {} + ); +}; + +const getValidValues = (ids: string[]): string[] => { + return ids.filter((id) => !isEmpty(id)); +}; diff --git a/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_detail.test.ts b/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_detail.test.ts new file mode 100644 index 0000000000000..55c154919cb10 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_detail.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { kibanaStartMock } from '../../utils/kibana_react.mock'; +import { TopAlert } from '../alerts'; +import * as pluginContext from '../../hooks/use_plugin_context'; +import { createObservabilityRuleTypeRegistryMock } from '../..'; +import { PluginContextValue } from '../../context/plugin_context'; +import { useFetchAlertDetail } from './use_fetch_alert_detail'; + +const mockUseKibanaReturnValue = kibanaStartMock.startContract(); + +jest.mock('../../utils/kibana_react', () => ({ + __esModule: true, + useKibana: jest.fn(() => mockUseKibanaReturnValue), +})); + +describe('useFetchAlertDetail', () => { + const getResult = { + 'kibana.alert.rule.category': 'Metric threshold', + 'kibana.alert.rule.consumer': 'infrastructure', + 'kibana.alert.rule.execution.uuid': 'e62c418d-734d-47e7-bbeb-e6f182f5fb45', + 'kibana.alert.rule.name': 'A super rule', + 'kibana.alert.rule.producer': 'infrastructure', + 'kibana.alert.rule.rule_type_id': 'metrics.alert.threshold', + 'kibana.alert.rule.uuid': '69411af0-82a2-11ec-8139-c1568734434e', + 'kibana.space_ids': ['default'], + 'kibana.alert.rule.tags': [], + '@timestamp': '2022-01-31T18:20:57.204Z', + 'kibana.alert.reason': 'Document count reported no data in the last 1 hour for all hosts', + 'kibana.alert.duration.us': 13793555000, + 'kibana.alert.instance.id': '*', + 'kibana.alert.start': '2022-01-31T14:31:03.649Z', + 'kibana.alert.uuid': '73c0d0cd-2df4-4550-862c-1d447e9c1db2', + 'kibana.alert.status': 'active', + 'kibana.alert.workflow_status': 'open', + 'event.kind': 'signal', + 'event.action': 'active', + 'kibana.version': '8.1.0', + tags: [], + }; + + const id = '123'; + const ruleType = createObservabilityRuleTypeRegistryMock(); + + beforeEach(() => { + mockUseKibanaReturnValue.services.http.get.mockImplementation(async () => getResult); + jest.spyOn(pluginContext, 'usePluginContext').mockImplementation( + () => + ({ + observabilityRuleTypeRegistry: ruleType, + } as unknown as PluginContextValue) + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('initially is not loading and does not have data', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useFetchAlertDetail(id) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual([false, null]); + }); + }); + + it('returns no data when an error occurs', async () => { + mockUseKibanaReturnValue.services.http.get.mockImplementation(async () => { + throw new Error('an http error'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useFetchAlertDetail('123') + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual([false, null]); + }); + }); + + it('retrieves the alert data', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useFetchAlertDetail(id) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toMatchInlineSnapshot(` + Array [ + false, + Object { + "0": "a", + "1": " ", + "2": "r", + "3": "e", + "4": "a", + "5": "s", + "6": "o", + "7": "n", + "active": true, + "fields": Object { + "@timestamp": "2022-01-31T18:20:57.204Z", + "event.action": "active", + "event.kind": "signal", + "kibana.alert.duration.us": 13793555000, + "kibana.alert.instance.id": "*", + "kibana.alert.reason": "Document count reported no data in the last 1 hour for all hosts", + "kibana.alert.rule.category": "Metric threshold", + "kibana.alert.rule.consumer": "infrastructure", + "kibana.alert.rule.execution.uuid": "e62c418d-734d-47e7-bbeb-e6f182f5fb45", + "kibana.alert.rule.name": "A super rule", + "kibana.alert.rule.producer": "infrastructure", + "kibana.alert.rule.rule_type_id": "metrics.alert.threshold", + "kibana.alert.rule.tags": Array [], + "kibana.alert.rule.uuid": "69411af0-82a2-11ec-8139-c1568734434e", + "kibana.alert.start": "2022-01-31T14:31:03.649Z", + "kibana.alert.status": "active", + "kibana.alert.uuid": "73c0d0cd-2df4-4550-862c-1d447e9c1db2", + "kibana.alert.workflow_status": "open", + "kibana.space_ids": Array [ + "default", + ], + "kibana.version": "8.1.0", + "tags": Array [], + }, + "link": undefined, + "reason": "Document count reported no data in the last 1 hour for all hosts", + "start": 1643639463649, + }, + ] + `); + }); + }); + + it('does not populate the results when the request is canceled', async () => { + await act(async () => { + const { result, waitForNextUpdate, unmount } = renderHook( + () => useFetchAlertDetail('123') + ); + + await waitForNextUpdate(); + unmount(); + + expect(result.current).toEqual([false, null]); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_detail.ts b/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_detail.ts new file mode 100644 index 0000000000000..4fe66a754056b --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/use_fetch_alert_detail.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useCallback, useMemo } from 'react'; +import { isEmpty } from 'lodash'; + +import { HttpSetup } from 'kibana/public'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { TopAlert, parseAlert } from '../../pages/alerts/'; +import { BASE_RAC_ALERTS_API_PATH } from '../../../../rule_registry/common/constants'; +import { ObservabilityRuleTypeRegistry } from '../..'; +import { useDataFetcher } from './use_data_fetcher'; + +interface AlertDetailParams { + id: string; + ruleType: ObservabilityRuleTypeRegistry; +} + +export const useFetchAlertDetail = (id: string): [boolean, TopAlert | null] => { + const { observabilityRuleTypeRegistry } = usePluginContext(); + + const params = useMemo( + () => ({ id, ruleType: observabilityRuleTypeRegistry }), + [id, observabilityRuleTypeRegistry] + ); + + const shouldExecuteApiCall = useCallback( + (apiCallParams: AlertDetailParams) => !isEmpty(apiCallParams.id), + [] + ); + + const { loading, data: alert } = useDataFetcher({ + paramsForApiCall: params, + initialDataState: null, + executeApiCall: fetchAlert, + shouldExecuteApiCall, + }); + + return [loading, alert]; +}; + +const fetchAlert = async ( + params: AlertDetailParams, + abortController: AbortController, + http: HttpSetup +): Promise => { + const { id, ruleType } = params; + try { + const response = await http.get>(BASE_RAC_ALERTS_API_PATH, { + query: { + id, + }, + signal: abortController.signal, + }); + if (response !== undefined) { + return parseAlert(ruleType)(response); + } + } catch (error) { + // ignore error for retrieving alert + } + + return null; +}; diff --git a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx index a6af0d9182215..b14628b2ae041 100644 --- a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx +++ b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTrackPageview } from '../..'; import { EmptySections } from '../../components/app/empty_sections'; import { ObservabilityHeaderMenu } from '../../components/app/header'; @@ -59,6 +59,20 @@ export function OverviewPage({ routeParams }: Props) { const { hasDataMap, hasAnyData, isAllRequestsComplete } = useHasData(); + const bucketSize = calculateBucketSize({ + start: absoluteTime.start, + end: absoluteTime.end, + }); + + const bucketSizeValue = useMemo(() => { + if (bucketSize?.bucketSize) { + return { + bucketSize: bucketSize.bucketSize, + intervalString: bucketSize.intervalString, + }; + } + }, [bucketSize?.bucketSize, bucketSize?.intervalString]); + if (hasAnyData === undefined) { return ; } @@ -73,11 +87,6 @@ export function OverviewPage({ routeParams }: Props) { const { refreshInterval = 10000, refreshPaused = true } = routeParams.query; - const bucketSize = calculateBucketSize({ - start: absoluteTime.start, - end: absoluteTime.end, - }); - return ( {/* Data sections */} - {hasAnyData && } + {hasAnyData && } diff --git a/x-pack/plugins/remote_clusters/kibana.json b/x-pack/plugins/remote_clusters/kibana.json index 192a1308c265a..2c5e0c3cea831 100644 --- a/x-pack/plugins/remote_clusters/kibana.json +++ b/x-pack/plugins/remote_clusters/kibana.json @@ -6,7 +6,7 @@ "name": "Stack Management", "githubTeam": "kibana-stack-management" }, - "requiredPlugins": ["licensing", "management", "indexManagement", "features"], + "requiredPlugins": ["licensing", "management", "indexManagement", "features", "share"], "optionalPlugins": ["usageCollection", "cloud"], "server": true, "ui": true, diff --git a/x-pack/plugins/remote_clusters/public/locator.ts b/x-pack/plugins/remote_clusters/public/locator.ts new file mode 100644 index 0000000000000..c6ea66f4c4328 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/locator.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SerializableRecord } from '@kbn/utility-types'; +import { ManagementAppLocator } from 'src/plugins/management/common'; +import { LocatorDefinition } from '../../../../src/plugins/share/public/'; + +export const REMOTE_CLUSTERS_LOCATOR_ID = 'REMOTE_CLUSTERS_LOCATOR'; + +export interface RemoteClustersLocatorParams extends SerializableRecord { + page: 'remoteClusters'; +} + +export interface RemoteClustersLocatorDefinitionDependencies { + managementAppLocator: ManagementAppLocator; +} + +export class RemoteClustersLocatorDefinition + implements LocatorDefinition +{ + constructor(protected readonly deps: RemoteClustersLocatorDefinitionDependencies) {} + + public readonly id = REMOTE_CLUSTERS_LOCATOR_ID; + + public readonly getLocation = async (params: RemoteClustersLocatorParams) => { + const location = await this.deps.managementAppLocator.getLocation({ + sectionId: 'data', + appId: 'remote_clusters', + }); + + switch (params.page) { + case 'remoteClusters': { + return { + ...location, + path: location.path, + }; + } + } + }; +} diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts index 4b47d76944b77..c6de539d1e6ed 100644 --- a/x-pack/plugins/remote_clusters/public/plugin.ts +++ b/x-pack/plugins/remote_clusters/public/plugin.ts @@ -16,6 +16,7 @@ import { init as initUiMetric } from './application/services/ui_metric'; import { init as initNotification } from './application/services/notification'; import { init as initRedirect } from './application/services/redirect'; import { Dependencies, ClientConfigType } from './types'; +import { RemoteClustersLocatorDefinition } from './locator'; export interface RemoteClustersPluginSetup { isUiEnabled: boolean; @@ -28,7 +29,7 @@ export class RemoteClustersUIPlugin setup( { notifications: { toasts }, http, getStartServices }: CoreSetup, - { management, usageCollection, cloud }: Dependencies + { management, usageCollection, cloud, share }: Dependencies ) { const { ui: { enabled: isRemoteClustersUiEnabled }, @@ -79,6 +80,12 @@ export class RemoteClustersUIPlugin }; }, }); + + share.url.locators.create( + new RemoteClustersLocatorDefinition({ + managementAppLocator: management.locator, + }) + ); } return { diff --git a/x-pack/plugins/remote_clusters/public/types.ts b/x-pack/plugins/remote_clusters/public/types.ts index bcd162599ab77..ad26e388c9fcd 100644 --- a/x-pack/plugins/remote_clusters/public/types.ts +++ b/x-pack/plugins/remote_clusters/public/types.ts @@ -8,6 +8,7 @@ import { ManagementSetup } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { RegisterManagementAppArgs } from 'src/plugins/management/public'; +import { SharePluginSetup } from 'src/plugins/share/public'; import { I18nStart } from 'kibana/public'; import { CloudSetup } from '../../cloud/public'; @@ -15,6 +16,7 @@ export interface Dependencies { management: ManagementSetup; usageCollection: UsageCollectionSetup; cloud: CloudSetup; + share: SharePluginSetup; } export interface ClientConfigType { diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 466ac2decefa1..39edef1cb6b0d 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -19,7 +19,7 @@ import { PluginInitializerContext, ThemeServiceStart, } from 'src/core/public'; -import type { ScreenshottingSetup } from '../../screenshotting/public'; +import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; import { FeatureCatalogueCategory, @@ -79,7 +79,7 @@ export interface ReportingPublicPluginSetupDendencies { home: HomePublicPluginSetup; management: ManagementSetup; uiActions: UiActionsSetup; - screenshotting: ScreenshottingSetup; + screenshotMode: ScreenshotModePluginSetup; share: SharePluginSetup; } @@ -152,7 +152,7 @@ export class ReportingPublicPlugin setupDeps: ReportingPublicPluginSetupDendencies ) { const { getStartServices, uiSettings } = core; - const { home, management, screenshotting, share, uiActions } = setupDeps; + const { home, management, screenshotMode, share, uiActions } = setupDeps; const startServices$ = Rx.from(getStartServices()); const usesUiCapabilities = !this.config.roles.enabled; @@ -209,7 +209,7 @@ export class ReportingPublicPlugin id: 'reportingRedirect', mount: async (params) => { const { mountRedirectApp } = await import('./redirect'); - return mountRedirectApp({ ...params, apiClient, screenshotting, share }); + return mountRedirectApp({ ...params, apiClient, screenshotMode, share }); }, title: 'Reporting redirect app', searchable: false, diff --git a/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx index fa658126efebc..20a6ee4eebac0 100644 --- a/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx +++ b/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiErrorBoundary } from '@elastic/eui'; import type { AppMountParameters } from 'kibana/public'; -import type { ScreenshottingSetup } from '../../../screenshotting/public'; +import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; import type { SharePluginSetup } from '../shared_imports'; import type { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -18,7 +18,7 @@ import { RedirectApp } from './redirect_app'; interface MountParams extends AppMountParameters { apiClient: ReportingAPIClient; - screenshotting: ScreenshottingSetup; + screenshotMode: ScreenshotModePluginSetup; share: SharePluginSetup; } @@ -26,7 +26,7 @@ export const mountRedirectApp = ({ element, apiClient, history, - screenshotting, + screenshotMode, share, }: MountParams) => { render( @@ -34,7 +34,7 @@ export const mountRedirectApp = ({ , diff --git a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx index 9f0b3f51f2731..31b5f2fa454de 100644 --- a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx +++ b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; import type { ScopedHistory } from 'src/core/public'; -import type { ScreenshottingSetup } from '../../../screenshotting/public'; +import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../../common/constants'; import { LocatorParams } from '../../common/types'; @@ -25,7 +25,7 @@ import './redirect_app.scss'; interface Props { apiClient: ReportingAPIClient; history: ScopedHistory; - screenshotting: ScreenshottingSetup; + screenshotMode: ScreenshotModePluginSetup; share: SharePluginSetup; } @@ -41,9 +41,7 @@ const i18nTexts = { ), }; -type ReportingContext = Record; - -export const RedirectApp: FunctionComponent = ({ apiClient, screenshotting, share }) => { +export const RedirectApp: FunctionComponent = ({ apiClient, screenshotMode, share }) => { const [error, setError] = useState(); useEffect(() => { @@ -57,8 +55,9 @@ export const RedirectApp: FunctionComponent = ({ apiClient, screenshottin const result = await apiClient.getInfo(jobId as string); locatorParams = result?.locatorParams?.[0]; } else { - locatorParams = - screenshotting.getContext()?.[REPORTING_REDIRECT_LOCATOR_STORE_KEY]; + locatorParams = screenshotMode.getScreenshotContext( + REPORTING_REDIRECT_LOCATOR_STORE_KEY + ); } if (!locatorParams) { @@ -73,7 +72,7 @@ export const RedirectApp: FunctionComponent = ({ apiClient, screenshottin throw e; } })(); - }, [apiClient, screenshotting, share]); + }, [apiClient, screenshotMode, share]); return (
diff --git a/x-pack/plugins/screenshotting/common/context.ts b/x-pack/plugins/screenshotting/common/context.ts deleted file mode 100644 index c47f8706533b8..0000000000000 --- a/x-pack/plugins/screenshotting/common/context.ts +++ /dev/null @@ -1,17 +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. - */ - -/** - * Screenshot context. - * This is a serializable object that can be passed from the screenshotting backend and then deserialized on the target page. - */ -export type Context = Record; - -/** - * @interal - */ -export const SCREENSHOTTING_CONTEXT_KEY = '__SCREENSHOTTING_CONTEXT_KEY__'; diff --git a/x-pack/plugins/screenshotting/common/index.ts b/x-pack/plugins/screenshotting/common/index.ts index 04296dd5426b5..1492f3f945abe 100644 --- a/x-pack/plugins/screenshotting/common/index.ts +++ b/x-pack/plugins/screenshotting/common/index.ts @@ -5,6 +5,5 @@ * 2.0. */ -export type { Context } from './context'; export type { LayoutParams } from './layout'; export { LayoutTypes } from './layout'; diff --git a/x-pack/plugins/screenshotting/kibana.json b/x-pack/plugins/screenshotting/kibana.json index 32446551627e0..9a071bdf46f46 100644 --- a/x-pack/plugins/screenshotting/kibana.json +++ b/x-pack/plugins/screenshotting/kibana.json @@ -9,6 +9,5 @@ "description": "Kibana Screenshotting Plugin", "requiredPlugins": ["screenshotMode"], "configPath": ["xpack", "screenshotting"], - "server": true, - "ui": true + "server": true } diff --git a/x-pack/plugins/screenshotting/public/context_storage.ts b/x-pack/plugins/screenshotting/public/context_storage.ts deleted file mode 100644 index 76a2cf231cf83..0000000000000 --- a/x-pack/plugins/screenshotting/public/context_storage.ts +++ /dev/null @@ -1,20 +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 { Context, SCREENSHOTTING_CONTEXT_KEY } from '../common/context'; - -declare global { - interface Window { - [SCREENSHOTTING_CONTEXT_KEY]?: Context; - } -} - -export class ContextStorage { - get(): T { - return (window[SCREENSHOTTING_CONTEXT_KEY] ?? {}) as T; - } -} diff --git a/x-pack/plugins/screenshotting/public/index.ts b/x-pack/plugins/screenshotting/public/index.ts deleted file mode 100644 index 659dbc81917a7..0000000000000 --- a/x-pack/plugins/screenshotting/public/index.ts +++ /dev/null @@ -1,18 +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 { ScreenshottingPlugin } from './plugin'; - -/** - * Screenshotting plugin entry point. - */ -export function plugin(...args: ConstructorParameters) { - return new ScreenshottingPlugin(...args); -} - -export { LayoutTypes } from '../common'; -export type { ScreenshottingSetup, ScreenshottingStart } from './plugin'; diff --git a/x-pack/plugins/screenshotting/public/plugin.tsx b/x-pack/plugins/screenshotting/public/plugin.tsx deleted file mode 100755 index 4ba5046b8a881..0000000000000 --- a/x-pack/plugins/screenshotting/public/plugin.tsx +++ /dev/null @@ -1,42 +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 type { Plugin } from 'src/core/public'; -import { ContextStorage } from './context_storage'; - -/** - * Setup public contract. - */ -export interface ScreenshottingSetup { - /** - * Gathers screenshot context that has been set on the backend. - */ - getContext: ContextStorage['get']; -} - -/** - * Start public contract. - */ -export type ScreenshottingStart = ScreenshottingSetup; - -export class ScreenshottingPlugin implements Plugin { - private contextStorage = new ContextStorage(); - - setup(): ScreenshottingSetup { - return { - getContext: () => this.contextStorage.get(), - }; - } - - start(): ScreenshottingStart { - return { - getContext: () => this.contextStorage.get(), - }; - } - - stop() {} -} diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts index 57f987f866ec9..4e71194486d01 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts @@ -10,12 +10,10 @@ import open from 'opn'; import puppeteer, { ElementHandle, EvaluateFn, Page, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; import { Logger } from 'src/core/server'; -import type { Layout } from 'src/plugins/screenshot_mode/common'; import { KBN_SCREENSHOT_MODE_HEADER, ScreenshotModePluginSetup, } from '../../../../../../src/plugins/screenshot_mode/server'; -import { Context, SCREENSHOTTING_CONTEXT_KEY } from '../../../common/context'; import { ConfigType } from '../../config'; import { allowRequest } from '../network_policy'; @@ -31,6 +29,8 @@ export interface ConditionalHeaders { conditions: ConditionalHeadersConditions; } +export type Context = Record; + export interface ElementPosition { boundingClientRect: { // modern browsers support x/y, but older ones don't @@ -56,7 +56,6 @@ interface OpenOptions { context?: Context; waitForSelector: string; timeout: number; - layout?: Layout; } interface WaitForSelectorOpts { @@ -124,13 +123,7 @@ export class HeadlessChromiumDriver { */ async open( url: string, - { - conditionalHeaders, - context, - layout, - waitForSelector: pageLoadSelector, - timeout, - }: OpenOptions, + { conditionalHeaders, context, waitForSelector: pageLoadSelector, timeout }: OpenOptions, logger: Logger ): Promise { logger.info(`opening url ${url}`); @@ -144,23 +137,8 @@ export class HeadlessChromiumDriver { */ await this.page.evaluateOnNewDocument(this.screenshotMode.setScreenshotModeEnabled); - if (context) { - await this.page.evaluateOnNewDocument( - (key: string, value: unknown) => { - Object.defineProperty(window, key, { - configurable: false, - writable: true, - enumerable: true, - value, - }); - }, - SCREENSHOTTING_CONTEXT_KEY, - context - ); - } - - if (layout) { - await this.page.evaluateOnNewDocument(this.screenshotMode.setScreenshotLayout, layout); + for (const [key, value] of Object.entries(context ?? {})) { + await this.page.evaluateOnNewDocument(this.screenshotMode.setScreenshotContext, key, value); } await this.page.setRequestInterception(true); diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts index 31a48cae7475e..88572081c5491 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts @@ -9,7 +9,7 @@ export const getChromiumDisconnectedError = () => new Error('Browser was closed unexpectedly! Check the server logs for more info.'); export { HeadlessChromiumDriver } from './driver'; -export type { ConditionalHeaders } from './driver'; +export type { ConditionalHeaders, Context } from './driver'; export { DEFAULT_VIEWPORT, HeadlessChromiumDriverFactory } from './driver_factory'; export type { PerformanceMetrics } from './driver_factory'; export { ChromiumArchivePaths } from './paths'; diff --git a/x-pack/plugins/screenshotting/server/browsers/index.ts b/x-pack/plugins/screenshotting/server/browsers/index.ts index ef5069ae51112..51687d74ff31d 100644 --- a/x-pack/plugins/screenshotting/server/browsers/index.ts +++ b/x-pack/plugins/screenshotting/server/browsers/index.ts @@ -7,7 +7,7 @@ export { download } from './download'; export { install } from './install'; -export type { ConditionalHeaders, PerformanceMetrics } from './chromium'; +export type { ConditionalHeaders, Context, PerformanceMetrics } from './chromium'; export { getChromiumDisconnectedError, ChromiumArchivePaths, diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts index a238af5bcc25b..5cfda2c841cb8 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -9,8 +9,7 @@ import type { Transaction } from 'elastic-apm-node'; import { defer, forkJoin, throwError, Observable } from 'rxjs'; import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; import type { Logger } from 'src/core/server'; -import type { Layout as ScreenshotModeLayout } from 'src/plugins/screenshot_mode/common'; -import type { ConditionalHeaders, HeadlessChromiumDriver } from '../browsers'; +import type { ConditionalHeaders, Context, HeadlessChromiumDriver } from '../browsers'; import { getChromiumDisconnectedError, DEFAULT_VIEWPORT } from '../browsers'; import type { Layout } from '../layouts'; import type { ElementsPositionAndAttribute } from './get_element_position_data'; @@ -22,7 +21,6 @@ import type { Screenshot } from './get_screenshots'; import { getTimeRange } from './get_time_range'; import { injectCustomCss } from './inject_css'; import { openUrl } from './open_url'; -import type { UrlOrUrlWithContext } from './open_url'; import { waitForRenderComplete } from './wait_for_render'; import { waitForVisualizations } from './wait_for_visualizations'; @@ -48,6 +46,10 @@ export interface PhaseTimeouts { loadDelay: number; } +type Url = string; +type UrlWithContext = [url: Url, context: Context]; +export type UrlOrUrlWithContext = Url | UrlWithContext; + export interface ScreenshotObservableOptions { /** * The browser timezone that will be emulated in the browser instance. @@ -157,18 +159,27 @@ export class ScreenshotObservableHandler { ); } - private openUrl(index: number, url: UrlOrUrlWithContext) { - return defer(() => - openUrl( + private openUrl(index: number, urlOrUrlWithContext: UrlOrUrlWithContext) { + return defer(() => { + let url: string; + let context: Context | undefined; + + if (typeof urlOrUrlWithContext === 'string') { + url = urlOrUrlWithContext; + } else { + [url, context] = urlOrUrlWithContext; + } + + return openUrl( this.driver, this.logger, this.options.timeouts.openUrl, index, url, - this.options.conditionalHeaders, - this.layout.id as ScreenshotModeLayout - ) - ).pipe(this.waitUntil(this.options.timeouts.openUrl, 'open URL')); + { ...(context ?? {}), layout: this.layout.id }, + this.options.conditionalHeaders + ); + }).pipe(this.waitUntil(this.options.timeouts.openUrl, 'open URL')); } private waitForElements() { diff --git a/x-pack/plugins/screenshotting/server/screenshots/open_url.ts b/x-pack/plugins/screenshotting/server/screenshots/open_url.ts index a27c9b5db4090..a4c4bd6217205 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/open_url.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/open_url.ts @@ -7,24 +7,18 @@ import apm from 'elastic-apm-node'; import type { Logger } from 'src/core/server'; -import type { Layout } from 'src/plugins/screenshot_mode/common'; -import { Context } from '../../common'; import type { HeadlessChromiumDriver } from '../browsers'; -import type { ConditionalHeaders } from '../browsers'; +import type { ConditionalHeaders, Context } from '../browsers'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; -type Url = string; -type UrlWithContext = [url: Url, context: Context]; -export type UrlOrUrlWithContext = Url | UrlWithContext; - export const openUrl = async ( browser: HeadlessChromiumDriver, logger: Logger, timeout: number, index: number, - urlOrUrlWithContext: UrlOrUrlWithContext, - conditionalHeaders: ConditionalHeaders, - layout?: Layout + url: string, + context: Context, + conditionalHeaders: ConditionalHeaders ): Promise => { // If we're moving to another page in the app, we'll want to wait for the app to tell us // it's loaded the next page. @@ -32,21 +26,8 @@ export const openUrl = async ( const waitForSelector = page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR; const span = apm.startSpan('open_url', 'wait'); - let url: string; - let context: Context | undefined; - - if (typeof urlOrUrlWithContext === 'string') { - url = urlOrUrlWithContext; - } else { - [url, context] = urlOrUrlWithContext; - } - try { - await browser.open( - url, - { conditionalHeaders, context, layout, waitForSelector, timeout }, - logger - ); + await browser.open(url, { conditionalHeaders, context, waitForSelector, timeout }, logger); } catch (err) { logger.error(err); throw new Error(`An error occurred when trying to open the Kibana URL: ${err.message}`); diff --git a/x-pack/plugins/screenshotting/tsconfig.json b/x-pack/plugins/screenshotting/tsconfig.json index a1e81c4fb38d9..bc9b65441f53d 100644 --- a/x-pack/plugins/screenshotting/tsconfig.json +++ b/x-pack/plugins/screenshotting/tsconfig.json @@ -8,7 +8,6 @@ }, "include": [ "common/**/*", - "public/**/*", "server/**/*", "../../../typings/**/*" ], diff --git a/x-pack/plugins/security_solution/public/cases/pages/use_fetch_alert_data.ts b/x-pack/plugins/security_solution/public/cases/pages/use_fetch_alert_data.ts index d75e7324c9afe..d39549a5bbac8 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/use_fetch_alert_data.ts +++ b/x-pack/plugins/security_solution/public/cases/pages/use_fetch_alert_data.ts @@ -12,7 +12,7 @@ import { useQueryAlerts } from '../../detections/containers/detection_engine/ale import { Ecs } from '../../../../cases/common'; import { buildAlertsQuery, formatAlertToEcsSignal, SignalHit } from '../../common/utils/alerts'; -export const useFetchAlertData = (alertIds: string[]): [boolean, Record] => { +export const useFetchAlertData = (alertIds: string[]): [boolean, Record] => { const { selectedPatterns } = useSourcererDataView(SourcererScopeName.detections); const alertsQuery = useMemo(() => buildAlertsQuery(alertIds), [alertIds]); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts index 49c084eb9f27d..c118eed5e2616 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts @@ -44,6 +44,7 @@ describe('ES deprecations table', () => { aliases: [], }, }); + httpRequestsMockHelpers.setLoadRemoteClustersResponse([]); await act(async () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); @@ -105,6 +106,25 @@ describe('ES deprecations table', () => { expect(find('warningDeprecationsCount').text()).toContain(warningDeprecations.length); }); + describe('remote clusters callout', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadRemoteClustersResponse(['test_remote_cluster']); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + }); + + it('shows a warning message if a user has remote clusters configured', () => { + const { exists } = testBed; + + // Verify warning exists + expect(exists('remoteClustersWarningCallout')).toBe(true); + }); + }); + describe('search bar', () => { it('filters results by "critical" status', async () => { const { find, actions } = testBed; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts index b29b6d4fbff37..774dfba4c1b9f 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts @@ -203,6 +203,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadRemoteClustersResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/remote_clusters`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadCloudBackupStatusResponse, setLoadEsDeprecationsResponse, @@ -220,6 +231,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setReindexStatusResponse, setLoadMlUpgradeModeResponse, setGetUpgradeStatusResponse, + setLoadRemoteClustersResponse, }; }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx index 85d8b260c1b79..1be57d33c935b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx @@ -109,7 +109,6 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ )} {(hasFetchFailed || hasReindexingFailed) && ( <> - {reindexState.errorMessage} + )} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx index d4bbab5102d99..b585ea20b1714 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useMemo } from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { EuiPageHeader, EuiSpacer, EuiPageContent, EuiLink } from '@elastic/eui'; +import { EuiPageHeader, EuiSpacer, EuiPageContent, EuiLink, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { DocLinksStart } from 'kibana/public'; @@ -51,6 +51,26 @@ const i18nTexts = { isLoading: i18n.translate('xpack.upgradeAssistant.esDeprecations.loadingText', { defaultMessage: 'Loading deprecation issues…', }), + remoteClustersDetectedTitle: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.remoteClustersDetectedTitle', + { + defaultMessage: 'Remote cluster compatibility', + } + ), + getRemoteClustersDetectedDescription: (remoteClustersCount: number) => + i18n.translate('xpack.upgradeAssistant.esDeprecations.remoteClustersDetectedDescription', { + defaultMessage: + 'You have {remoteClustersCount} {remoteClustersCount, plural, one {remote cluster} other {remote clusters}} configured. If you use cross-cluster search, note that 8.x can only search remote clusters running the previous minor version or later. If you use cross-cluster replication, a cluster that contains follower indices must run the same or newer version as the remote cluster.', + values: { + remoteClustersCount, + }, + }), + remoteClustersLinkText: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.remoteClustersLinkText', + { + defaultMessage: 'View remote clusters.', + } + ), }; const getBatchReindexLink = (docLinks: DocLinksStart) => { @@ -75,6 +95,22 @@ const getBatchReindexLink = (docLinks: DocLinksStart) => { ); }; +const RemoteClustersAppLink: React.FunctionComponent = () => { + const { + plugins: { share }, + } = useAppContext(); + + const remoteClustersUrl = share.url.locators + .get('REMOTE_CLUSTERS_LOCATOR') + ?.useUrl({ page: 'remoteClusters' }); + + return ( + + {i18nTexts.remoteClustersLinkText} + + ); +}; + export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { const { services: { @@ -85,6 +121,7 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { } = useAppContext(); const { data: esDeprecations, isLoading, error, resendRequest } = api.useLoadEsDeprecations(); + const { data: remoteClusters } = api.useLoadRemoteClusters(); const deprecationsCountByLevel: { warningDeprecations: number; @@ -140,10 +177,29 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { } > - + <> + {remoteClusters && remoteClusters.length > 0 && ( + <> + +

+ {i18nTexts.getRemoteClustersDetectedDescription(remoteClusters.length)}{' '} + +

+
+ + + )} + + + diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts index f3c5680b56e20..49be16b44236d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts @@ -239,6 +239,13 @@ export class ApiService { method: 'get', }); } + + public useLoadRemoteClusters() { + return this.useRequest({ + path: `${API_BASE_PATH}/remote_clusters`, + method: 'get', + }); + } } export const apiService = new ApiService(); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index 724b282a87747..3182a74498994 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -293,12 +293,8 @@ export const reindexServiceFactory = ( // Do any other cleanup work necessary reindexOp = await cleanupChanges(reindexOp); } else { - // Check that it reindexed all documents - const { - body: { count }, - } = await esClient.count({ index: reindexOp.attributes.indexName }); - - if (taskResponse.task.status!.created < count) { + // Check that no failures occurred + if (taskResponse.response?.failures?.length) { // Include the entire task result in the error message. This should be guaranteed // to be JSON-serializable since it just came back from Elasticsearch. throw error.reindexTaskFailed(`Reindexing failed: ${JSON.stringify(taskResponse)}`); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts index b6c8850376684..c0f422711901b 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts @@ -18,6 +18,7 @@ import { registerUpdateSettingsRoute } from './update_index_settings'; import { registerMlSnapshotRoutes } from './ml_snapshots'; import { ReindexWorker } from '../lib/reindexing'; import { registerUpgradeStatusRoute } from './status'; +import { registerRemoteClustersRoute } from './remote_clusters'; export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) { registerAppRoutes(dependencies); @@ -32,4 +33,5 @@ export function registerRoutes(dependencies: RouteDependencies, getWorker: () => registerMlSnapshotRoutes(dependencies); // Route for cloud to retrieve the upgrade status for ES and Kibana registerUpgradeStatusRoute(dependencies); + registerRemoteClustersRoute(dependencies); } diff --git a/x-pack/plugins/upgrade_assistant/server/routes/remote_clusters.ts b/x-pack/plugins/upgrade_assistant/server/routes/remote_clusters.ts new file mode 100644 index 0000000000000..de2177cffa1fe --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/remote_clusters.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { API_BASE_PATH } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +export function registerRemoteClustersRoute({ router, lib: { handleEsError } }: RouteDependencies) { + router.get( + { + path: `${API_BASE_PATH}/remote_clusters`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const { body: clustersByName } = await client.asCurrentUser.cluster.remoteInfo(); + + const remoteClusters = Object.keys(clustersByName); + + return response.ok({ body: remoteClusters }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); +} diff --git a/x-pack/plugins/uptime/e2e/config.ts b/x-pack/plugins/uptime/e2e/config.ts index 4d509d4045be9..48b4e048000f2 100644 --- a/x-pack/plugins/uptime/e2e/config.ts +++ b/x-pack/plugins/uptime/e2e/config.ts @@ -8,6 +8,12 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { readKibanaConfig } from './tasks/read_kibana_config'; + +const MANIFEST_KEY = 'xpack.uptime.service.manifestUrl'; +const SERVICE_PASSWORD = 'xpack.uptime.service.password'; +const SERVICE_USERNAME = 'xpack.uptime.service.username'; + async function config({ readConfigFile }: FtrConfigProviderContext) { const kibanaCommonTestsConfig = await readConfigFile( require.resolve('../../../../test/common/config.js') @@ -16,6 +22,12 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/functional/config.js') ); + const kibanaConfig = readKibanaConfig(); + + const manifestUrl = process.env.SYNTHETICS_SERVICE_MANIFEST ?? kibanaConfig[MANIFEST_KEY]; + const serviceUsername = process.env.SYNTHETICS_SERVICE_USERNAME ?? kibanaConfig[SERVICE_USERNAME]; + const servicPassword = process.env.SYNTHETICS_SERVICE_PASSWORD ?? kibanaConfig[SERVICE_PASSWORD]; + return { ...kibanaCommonTestsConfig.getAll(), @@ -44,9 +56,9 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { `--elasticsearch.username=kibana_system`, `--elasticsearch.password=changeme`, '--xpack.reporting.enabled=false', - `--xpack.uptime.service.manifestUrl=${process.env.SYNTHETICS_SERVICE_MANIFEST}`, - `--xpack.uptime.service.username=${process.env.SYNTHETICS_SERVICE_USERNAME}`, - `--xpack.uptime.service.password=${process.env.SYNTHETICS_SERVICE_PASSWORD}`, + `--xpack.uptime.service.manifestUrl=${manifestUrl}`, + `--xpack.uptime.service.username=${serviceUsername}`, + `--xpack.uptime.service.password=${servicPassword}`, '--xpack.uptime.ui.monitorManagement.enabled=true', ], }, diff --git a/x-pack/plugins/uptime/e2e/helpers/make_checks.ts b/x-pack/plugins/uptime/e2e/helpers/make_checks.ts new file mode 100644 index 0000000000000..b9e913524cb1f --- /dev/null +++ b/x-pack/plugins/uptime/e2e/helpers/make_checks.ts @@ -0,0 +1,182 @@ +/* + * 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 uuid from 'uuid'; +import { merge, flattenDeep } from 'lodash'; +import type { Client } from '@elastic/elasticsearch'; +import { makePing } from './make_ping'; +import { TlsProps } from './make_tls'; + +interface CheckProps { + es: Client; + monitorId?: string; + numIps?: number; + fields?: { [key: string]: any }; + mogrify?: (doc: any) => any; + refresh?: boolean; + tls?: boolean | TlsProps; + isFleetManaged?: boolean; +} + +const getRandomMonitorId = () => { + return 'monitor-' + Math.random().toString(36).substring(7); +}; +export const makeCheck = async ({ + es, + monitorId = getRandomMonitorId(), + numIps = 1, + fields = {}, + mogrify = (d) => d, + refresh = true, + tls = false, + isFleetManaged = false, +}: CheckProps): Promise<{ monitorId: string; docs: any }> => { + const cgFields = { + monitor: { + check_group: uuid.v4(), + }, + }; + + const docs = []; + const summary = { + up: 0, + down: 0, + }; + for (let i = 0; i < numIps; i++) { + const pingFields = merge(fields, cgFields, { + monitor: { + ip: `127.0.0.${i}`, + }, + }); + if (i === numIps - 1) { + pingFields.summary = summary; + } + const doc = await makePing( + es, + monitorId, + pingFields, + mogrify, + false, + tls as any, + isFleetManaged + ); + docs.push(doc); + // @ts-ignore + summary[doc.monitor.status]++; + } + + if (refresh) { + await es.indices.refresh(); + } + + return { monitorId, docs }; +}; + +export const makeChecks = async ( + es: Client, + monitorId: string, + numChecks: number = 1, + numIps: number = 1, + every: number = 10000, // number of millis between checks + fields: { [key: string]: any } = {}, + mogrify: (doc: any) => any = (d) => d, + refresh: boolean = true, + isFleetManaged: boolean = false +) => { + const checks = []; + const oldestTime = new Date().getTime() - numChecks * every; + let newestTime = oldestTime; + for (let li = 0; li < numChecks; li++) { + const checkDate = new Date(newestTime + every); + newestTime = checkDate.getTime() + every; + fields = merge(fields, { + '@timestamp': checkDate.toISOString(), + monitor: { + timespan: { + gte: checkDate.toISOString(), + lt: new Date(newestTime).toISOString(), + }, + }, + }); + const { docs } = await makeCheck({ + es, + monitorId, + numIps, + fields, + mogrify, + refresh: false, + isFleetManaged, + }); + checks.push(docs); + } + + if (refresh) { + await es.indices.refresh(); + } + + return checks; +}; + +export const makeChecksWithStatus = async ( + es: Client, + monitorId: string, + numChecks: number, + numIps: number, + every: number, + fields: { [key: string]: any } = {}, + status: 'up' | 'down', + mogrify: (doc: any) => any = (d) => d, + refresh: boolean = true, + isFleetManaged: boolean = false +) => { + const oppositeStatus = status === 'up' ? 'down' : 'up'; + + return await makeChecks( + es, + monitorId, + numChecks, + numIps, + every, + fields, + (d) => { + d.monitor.status = status; + if (d.summary) { + d.summary[status] += d.summary[oppositeStatus]; + d.summary[oppositeStatus] = 0; + } + + return mogrify(d); + }, + refresh, + isFleetManaged + ); +}; + +// Helper for processing a list of checks to find the time picker bounds. +export const getChecksDateRange = (checks: any[]) => { + // Flatten 2d arrays + const flattened = flattenDeep(checks); + + let startTime = 1 / 0; + let endTime = -1 / 0; + flattened.forEach((c) => { + const ts = Date.parse(c['@timestamp']); + + if (ts < startTime) { + startTime = ts; + } + + if (ts > endTime) { + endTime = ts; + } + }); + + return { + start: new Date(startTime).toISOString(), + end: new Date(endTime).toISOString(), + }; +}; diff --git a/x-pack/plugins/uptime/e2e/helpers/make_ping.ts b/x-pack/plugins/uptime/e2e/helpers/make_ping.ts new file mode 100644 index 0000000000000..c93d6f437268d --- /dev/null +++ b/x-pack/plugins/uptime/e2e/helpers/make_ping.ts @@ -0,0 +1,126 @@ +/* + * 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 uuid from 'uuid'; +import { merge } from 'lodash'; +import type { Client } from '@elastic/elasticsearch'; +import { makeTls, TlsProps } from './make_tls'; + +const DEFAULT_INDEX_NAME = 'heartbeat-8-full-test'; +const DATA_STREAM_INDEX_NAME = 'synthetics-http-default'; + +export const makePing = async ( + es: Client, + monitorId: string, + fields: { [key: string]: any }, + mogrify: (doc: any) => any, + refresh: boolean = true, + tls: boolean | TlsProps = false, + isFleetManaged: boolean | undefined = false +) => { + const timestamp = new Date(); + const baseDoc: any = { + tcp: { + rtt: { + connect: { + us: 14687, + }, + }, + }, + observer: { + geo: { + name: 'mpls', + location: '37.926868, -78.024902', + }, + hostname: 'avc-x1e', + }, + agent: { + hostname: 'avc-x1e', + id: '10730a1a-4cb7-45ce-8524-80c4820476ab', + type: 'heartbeat', + ephemeral_id: '0d9a8dc6-f604-49e3-86a0-d8f9d6f2cbad', + version: '8.0.0', + }, + '@timestamp': timestamp.toISOString(), + resolve: { + rtt: { + us: 350, + }, + ip: '127.0.0.1', + }, + ecs: { + version: '1.1.0', + }, + host: { + name: 'avc-x1e', + }, + http: { + rtt: { + response_header: { + us: 19349, + }, + total: { + us: 48954, + }, + write_request: { + us: 33, + }, + content: { + us: 51, + }, + validate: { + us: 19400, + }, + }, + response: { + status_code: 200, + body: { + bytes: 3, + hash: '27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf', + }, + }, + }, + monitor: { + duration: { + us: 49347, + }, + ip: '127.0.0.1', + id: monitorId, + check_group: uuid.v4(), + type: 'http', + status: 'up', + timespan: { + gte: timestamp.toISOString(), + lt: new Date(timestamp.getTime() + 5000).toISOString, + }, + }, + event: { + dataset: 'uptime', + }, + url: { + path: '/pattern', + scheme: 'http', + port: 5678, + domain: 'localhost', + query: 'r=200x5,500x1', + full: 'http://localhost:5678/pattern?r=200x5,500x1', + }, + }; + + if (tls) { + baseDoc.tls = makeTls(tls as any); + } + + const doc = mogrify(merge(baseDoc, fields)); + + await es.index({ + index: isFleetManaged ? DATA_STREAM_INDEX_NAME : DEFAULT_INDEX_NAME, + refresh, + body: doc, + }); + return doc; +}; diff --git a/x-pack/plugins/uptime/e2e/helpers/make_tls.ts b/x-pack/plugins/uptime/e2e/helpers/make_tls.ts new file mode 100644 index 0000000000000..e654a2754e51d --- /dev/null +++ b/x-pack/plugins/uptime/e2e/helpers/make_tls.ts @@ -0,0 +1,68 @@ +/* + * 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 moment from 'moment'; +import crypto from 'crypto'; + +export interface TlsProps { + valid?: boolean; + commonName?: string; + expiry?: string; + sha256?: string; +} + +type Props = TlsProps & boolean; + +// Note This is just a mock sha256 value, this doesn't actually generate actually sha 256 val +export const getSha256 = () => { + return crypto.randomBytes(64).toString('hex').toUpperCase(); +}; + +export const makeTls = ({ valid = true, commonName = '*.elastic.co', expiry, sha256 }: Props) => { + const expiryDate = + expiry ?? + moment() + .add(valid ? 2 : -2, 'months') + .toISOString(); + + return { + version: '1.3', + cipher: 'TLS-AES-128-GCM-SHA256', + certificate_not_valid_before: '2020-03-01T00:00:00.000Z', + certificate_not_valid_after: expiryDate, + server: { + x509: { + not_before: '2020-03-01T00:00:00.000Z', + not_after: expiryDate, + issuer: { + distinguished_name: + 'CN=DigiCert SHA2 High Assurance Server CA,OU=www.digicert.com,O=DigiCert Inc,C=US', + common_name: 'DigiCert SHA2 High Assurance Server CA', + }, + subject: { + common_name: commonName, + distinguished_name: 'CN=*.facebook.com,O=Facebook Inc.,L=Menlo Park,ST=California,C=US', + }, + serial_number: '10043199409725537507026285099403602396', + signature_algorithm: 'SHA256-RSA', + public_key_algorithm: 'ECDSA', + public_key_curve: 'P-256', + }, + hash: { + sha256: sha256 ?? '1a48f1db13c3bd1482ba1073441e74a1bb1308dc445c88749e0dc4f1889a88a4', + sha1: '23291c758d925b9f4bb3584de3763317e94c6ce9', + }, + }, + established: true, + rtt: { + handshake: { + us: 33103, + }, + }, + version_protocol: 'tls', + }; +}; diff --git a/x-pack/plugins/uptime/e2e/journeys/data_view_permissions.ts b/x-pack/plugins/uptime/e2e/journeys/data_view_permissions.ts index cf50e0d6b58ae..5d8d2a1a5d848 100644 --- a/x-pack/plugins/uptime/e2e/journeys/data_view_permissions.ts +++ b/x-pack/plugins/uptime/e2e/journeys/data_view_permissions.ts @@ -6,8 +6,7 @@ */ import { journey, step, expect, before } from '@elastic/synthetics'; -import { loginToKibana, waitForLoadingToFinish } from './utils'; -import { byTestId } from './uptime.journey'; +import { byTestId, loginToKibana, waitForLoadingToFinish } from './utils'; import { callKibana } from '../../../apm/scripts/create_apm_users_and_roles/helpers/call_kibana'; journey('DataViewPermissions', async ({ page, params }) => { diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index fe8a4960eac12..ae3450a992256 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -12,3 +12,4 @@ export * from './alerts'; export * from './read_only_user'; export * from './monitor_name.journey'; export * from './monitor_management.journey'; +export * from './monitor_details'; diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_details/index.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_details/index.ts new file mode 100644 index 0000000000000..51b782f118c2a --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_details/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export * from './monitor_details.journey'; +export * from './monitor_alerts.journey'; +export * from './ping_redirects.journey'; diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts new file mode 100644 index 0000000000000..c44dbc187bd53 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts @@ -0,0 +1,94 @@ +/* + * 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 { journey, step, expect, before, Page } from '@elastic/synthetics'; +import { noop } from 'lodash'; +import { monitorDetailsPageProvider } from '../../page_objects/monitor_details'; +import { byTestId, delay } from '../utils'; + +const dateRangeStart = '2019-09-10T12:40:08.078Z'; +const dateRangeEnd = '2019-09-11T19:40:08.078Z'; +const monitorId = '0000-intermittent'; + +const alertId = 'uptime-anomaly-alert'; +const alertThreshold = 'major'; + +journey('MonitorAlerts', async ({ page, params }: { page: Page; params: any }) => { + const monitorDetails = monitorDetailsPageProvider({ page, kibanaUrl: params.kibanaUrl }); + + before(async () => { + await monitorDetails.waitForLoadingToFinish(); + }); + + step('go to overview', async () => { + await monitorDetails.navigateToOverviewPage({ dateRangeEnd, dateRangeStart }); + }); + + step('login to Kibana', async () => { + await monitorDetails.loginToKibana(); + }); + + step('go to monitor details', async () => { + await monitorDetails.navigateToMonitorDetails(monitorId); + await monitorDetails.waitForLoadingToFinish(); + }); + + step('clean previous data if available', async () => { + // Should only happen locally + await monitorDetails.disableAnomalyDetectionAlert().catch(noop); + await monitorDetails.disableAnomalyDetection().catch(noop); + }); + + step('open anomaly detection flyout', async () => { + await monitorDetails.waitAndRefresh(5000); + await monitorDetails.enableAnomalyDetection(); + await monitorDetails.ensureAnomalyDetectionFlyoutIsOpen(); + }); + + step('can create job', async () => { + const canCreateJob = await monitorDetails.canCreateJob(); + const missingLicense = await page + .waitForSelector('uptimeMLLicenseInfo', { timeout: 10000 }) + .then(() => true) + .catch(() => false); + expect(canCreateJob).toBeTruthy(); + expect(missingLicense).toBeFalsy(); + }); + + step('creates ML job', async () => { + await page.click(byTestId('uptimeMLCreateJobBtn')); + await page.waitForSelector(byTestId('uptimeMLJobSuccessfullyCreated'), { timeout: 30000 }); + await page.click(byTestId('toastCloseButton')); + }); + + step('close anomaly detection flyout', async () => { + await page.click(byTestId('cancelSaveAlertButton')); + }); + + step('open anomaly detection alert', async () => { + await monitorDetails.waitAndRefresh(3000); + await monitorDetails.openAnomalyDetectionMenu(); + await page.click(byTestId('uptimeEnableAnomalyAlertBtn')); + }); + + step('update anomaly detection alert', async () => { + await monitorDetails.updateAlert({ id: alertId, threshold: alertThreshold }); + }); + + step('save anomaly detection alert', async () => { + await page.click(byTestId('saveAlertButton')); + await page.click(byTestId('confirmModalConfirmButton')); + await page.waitForSelector(`text=Created rule "${alertId}"`); + }); + + step('disable anomaly detection alert', async () => { + await monitorDetails.waitAndRefresh(5000); + await monitorDetails.disableAnomalyDetectionAlert(); + await delay(1000); // Menu has delay when closing + await monitorDetails.disableAnomalyDetection(); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_details.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_details.journey.ts new file mode 100644 index 0000000000000..2965c1acf2c2b --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_details.journey.ts @@ -0,0 +1,60 @@ +/* + * 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 { journey, step, before, Page } from '@elastic/synthetics'; +import { monitorDetailsPageProvider } from '../../page_objects/monitor_details'; +import { byTestId } from '../utils'; + +const dateRangeStart = '2019-09-10T12:40:08.078Z'; +const dateRangeEnd = '2019-09-11T19:40:08.078Z'; +const monitorId = '0000-intermittent'; + +journey('MonitorDetails', async ({ page, params }: { page: Page; params: any }) => { + const monitorDetails = monitorDetailsPageProvider({ page, kibanaUrl: params.kibanaUrl }); + + before(async () => { + await monitorDetails.waitForLoadingToFinish(); + }); + + step('go to overview', async () => { + await monitorDetails.navigateToOverviewPage({ dateRangeEnd, dateRangeStart }); + }); + + step('login to Kibana', async () => { + await monitorDetails.loginToKibana(); + }); + + step('go to monitor details', async () => { + await monitorDetails.navigateToMonitorDetails(monitorId); + await monitorDetails.waitForLoadingToFinish(); + }); + + step('should select the ping list location filter', async () => { + await monitorDetails.selectFilterItem('Location', 'mpls'); + }); + + step('should set the status filter', async () => { + await monitorDetails.setStatusFilterUp(); + }); + + step('displays ping data as expected', async () => { + await Promise.all( + [ + 'XZtoHm0B0I9WX_CznN-6', + '7ZtoHm0B0I9WX_CzJ96M', + 'pptnHm0B0I9WX_Czst5X', + 'I5tnHm0B0I9WX_CzPd46', + 'y5tmHm0B0I9WX_Czx93x', + 'XZtmHm0B0I9WX_CzUt3H', + '-JtlHm0B0I9WX_Cz3dyX', + 'k5tlHm0B0I9WX_CzaNxm', + 'NZtkHm0B0I9WX_Cz89w9', + 'zJtkHm0B0I9WX_CzftsN', + ].map((id) => page.waitForSelector(byTestId(`"xpack.uptime.pingList.ping-${id}"`))) + ); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_details/ping_redirects.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_details/ping_redirects.journey.ts new file mode 100644 index 0000000000000..ffbc322d7e779 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_details/ping_redirects.journey.ts @@ -0,0 +1,73 @@ +/* + * 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 { journey, step, expect, before, Page } from '@elastic/synthetics'; +import { makeChecksWithStatus } from '../../helpers/make_checks'; +import { monitorDetailsPageProvider } from '../../page_objects/monitor_details'; +import { byTestId, delay } from '../utils'; + +journey('MonitorPingRedirects', async ({ page, params }: { page: Page; params: any }) => { + const monitorDetails = monitorDetailsPageProvider({ page, kibanaUrl: params.kibanaUrl }); + const testMonitor = { + id: '0000-intermittent', + start: 'now-15m', + end: 'now', + redirects: ['http://localhost:3000/first', 'https://www.washingtonpost.com/'], + }; + + before(async () => { + await monitorDetails.waitForLoadingToFinish(); + await makeChecksWithStatus( + params.getService('es'), + testMonitor.id, + 5, + 2, + 10000, + { + http: { + rtt: { total: { us: 157784 } }, + response: { + status_code: 200, + redirects: testMonitor.redirects, + body: { + bytes: 642102, + hash: '597a8cfb33ff8e09bff16283306553c3895282aaf5386e1843d466d44979e28a', + }, + }, + }, + }, + 'up' + ); + await delay(5000); + }); + + step('go to monitor-management', async () => { + await monitorDetails.navigateToOverviewPage({ + dateRangeEnd: testMonitor.end, + dateRangeStart: testMonitor.start, + }); + }); + + step('login to Kibana', async () => { + await monitorDetails.loginToKibana(); + }); + + step('go to monitor details', async () => { + await monitorDetails.navigateToMonitorDetails(testMonitor.id); + }); + + step('displays redirect info in detail panel', async () => { + await monitorDetails.waitForLoadingToFinish(); + expect(await monitorDetails.getMonitorRedirects()).toEqual(`${testMonitor.redirects.length}`); + }); + + step('displays redirects in ping list expand row', async () => { + await monitorDetails.expandPingDetails(); + await monitorDetails.waitForLoadingToFinish(); + await page.waitForSelector(byTestId('uptimeMonitorPingListRedirectInfo')); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts index 0286f7e898add..d2decba3e9a99 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts @@ -8,6 +8,7 @@ import { journey, step, expect, before, after, Page } from '@elastic/synthetics'; import { monitorManagementPageProvider } from '../page_objects/monitor_management'; import { DataStream } from '../../common/runtime_types/monitor_management'; +import { byTestId } from './utils'; const basicMonitorDetails = { location: 'US Central', @@ -197,6 +198,8 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page; step('edit http monitor and check breadcrumb', async () => { await uptime.editMonitor(); + // breadcrumb is available before edit page is loaded, make sure its edit view + await page.waitForSelector(byTestId('monitorManagementMonitorName')); const breadcrumbs = await page.$$('[data-test-subj=breadcrumb]'); expect(await breadcrumbs[1].textContent()).toEqual('Monitor management'); const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent(); diff --git a/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts b/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts index 1c3f82c517b82..7a256e1157272 100644 --- a/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts @@ -6,11 +6,7 @@ */ import { journey, step, before } from '@elastic/synthetics'; -import { waitForLoadingToFinish } from './utils'; - -export const byTestId = (testId: string) => { - return `[data-test-subj=${testId}]`; -}; +import { byTestId, waitForLoadingToFinish } from './utils'; journey('uptime', ({ page, params }) => { before(async () => { diff --git a/x-pack/plugins/uptime/e2e/journeys/utils.ts b/x-pack/plugins/uptime/e2e/journeys/utils.ts index 8dbc2699a438f..a042d37a99375 100644 --- a/x-pack/plugins/uptime/e2e/journeys/utils.ts +++ b/x-pack/plugins/uptime/e2e/journeys/utils.ts @@ -44,3 +44,11 @@ export const assertText = async ({ page, text }: { page: Page; text: string }) = export const assertNotText = async ({ page, text }: { page: Page; text: string }) => { expect(await page.$(`text=${text}`)).toBeFalsy(); }; + +export const getQuerystring = (params: object) => { + return Object.entries(params) + .map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value)) + .join('&'); +}; + +export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx new file mode 100644 index 0000000000000..efadc26a383c0 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx @@ -0,0 +1,129 @@ +/* + * 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 { Page } from '@elastic/synthetics'; +import { byTestId, delay } from '../journeys/utils'; +import { monitorManagementPageProvider } from './monitor_management'; + +interface AlertType { + id: string; + threshold: string; +} + +export function monitorDetailsPageProvider({ page, kibanaUrl }: { page: Page; kibanaUrl: string }) { + return { + ...monitorManagementPageProvider({ page, kibanaUrl }), + + async navigateToMonitorDetails(monitorId: string) { + await page.click(byTestId(`monitor-page-link-${monitorId}`)); + }, + + async selectFilterItem(filterType: string, itemArg: string | string[]) { + const itemList = Array.isArray(itemArg) ? itemArg : [itemArg]; + await page.click(`[aria-label="expands filter group for ${filterType} filter"]`); + await this.clickFilterItems(itemList); + return this.applyFilterItems(filterType); + }, + + async clickFilterItems(itemList: string[]) { + for (const title of itemList) { + await page.click(`li[title="${title}"]`); + } + }, + + async applyFilterItems(filterType: string) { + await page.click(`[aria-label="Apply the selected filters for ${filterType}"]`); + }, + + async setStatusFilterUp() { + await page.click('[data-test-subj="xpack.uptime.filterBar.filterStatusUp"]'); + }, + + async setStatusFilterDown() { + await page.click('[data-test-subj="xpack.uptime.filterBar.filterStatusDown"]'); + }, + + async refreshFromES() { + await this.byTestId('superDatePickerApplyTimeButton'); + }, + + async enableAnomalyDetection() { + await page.click(byTestId('uptimeEnableAnomalyBtn')); + }, + + async getMonitorRedirects() { + return await page.textContent(byTestId('uptimeMonitorRedirectInfo')); + }, + + async expandPingDetails() { + await page.click(byTestId('uptimePingListExpandBtn')); + }, + + async ensureAnomalyDetectionFlyoutIsOpen() { + await page.waitForSelector(byTestId('uptimeMLFlyout')); + }, + + async isMLMenuVisible() { + return await page.isVisible(byTestId('uptimeManageMLContextMenu'), { + timeout: 3000, + }); + }, + + async canCreateJob(): Promise { + await this.ensureAnomalyDetectionFlyoutIsOpen(); + const createJobBtn = await page.$(byTestId('uptimeMLCreateJobBtn')); + return await createJobBtn!.isEnabled(); + }, + + async openAnomalyDetectionMenu() { + const visible = await this.isMLMenuVisible(); + if (visible === false) { + await page.click(byTestId('uptimeManageMLJobBtn'), { timeout: 5000 }); + } + }, + + async closeAnomalyDetectionMenu() { + if ((await this.isMLMenuVisible()) === true) { + await page.click(byTestId('uptimeManageMLJobBtn'), { timeout: 5000 }); + } + }, + + async waitAndRefresh(timeout?: number) { + await delay(timeout ?? 1000); + await this.refreshFromES(); + await this.waitForLoadingToFinish(); + }, + + async updateAlert({ id, threshold }: AlertType) { + await this.fillByTestSubj('alertNameInput', id); + await this.selectAlertThreshold(threshold); + }, + + async selectAlertThreshold(threshold: string) { + await this.clickByTestSubj('uptimeAnomalySeverity'); + await this.clickByTestSubj('anomalySeveritySelect'); + await page.click(`text=${threshold}`); + }, + + async disableAnomalyDetection() { + await this.openAnomalyDetectionMenu(); + await page.click(byTestId('uptimeDeleteMLJobBtn'), { timeout: 10000 }); + await page.click(byTestId('confirmModalConfirmButton')); + await page.waitForSelector('text=Job deleted'); + await this.closeAnomalyDetectionMenu(); + }, + + async disableAnomalyDetectionAlert() { + await this.openAnomalyDetectionMenu(); + await page.click(byTestId('uptimeManageAnomalyAlertBtn'), { timeout: 10000 }); + await page.click(byTestId('uptimeDisableAnomalyAlertBtn')); + await page.click(byTestId('confirmModalConfirmButton')); + await page.waitForSelector('text=Rule successfully disabled!'); + await this.closeAnomalyDetectionMenu(); + }, + }; +} diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx index fd877708f2bce..a19f14fa1a6d1 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx @@ -7,6 +7,7 @@ import { Page } from '@elastic/synthetics'; import { DataStream } from '../../common/runtime_types/monitor_management'; +import { getQuerystring } from '../journeys/utils'; import { loginPageProvider } from './login'; import { utilsPageProvider } from './utils'; @@ -46,8 +47,8 @@ export function monitorManagementPageProvider({ }); }, - async navigateToOverviewPage() { - await page.goto(overview, { + async navigateToOverviewPage(options?: object) { + await page.goto(`${overview}${options ? `?${getQuerystring(options)}` : ''}`, { waitUntil: 'networkidle', }); }, @@ -58,11 +59,12 @@ export function monitorManagementPageProvider({ async deleteMonitor() { await this.clickByTestSubj('monitorManagementDeleteMonitor'); + await this.clickByTestSubj('confirmModalConfirmButton'); return await this.findByTestSubj('uptimeDeleteMonitorSuccess'); }, async editMonitor() { - await this.clickByTestSubj('monitorManagementEditMonitor'); + await page.click(this.byTestId('monitorManagementEditMonitor'), { delay: 800 }); }, async findMonitorConfiguration(monitorConfig: Record) { @@ -96,9 +98,8 @@ export function monitorManagementPageProvider({ }, async selectLocations({ locations }: { locations: string[] }) { - await this.clickByTestSubj('syntheticsServiceLocationsComboBox'); for (let i = 0; i < locations.length; i++) { - await page.click(`text=${locations[i]}`); + await page.check(`text=${locations[i]}`); } }, diff --git a/x-pack/plugins/uptime/e2e/page_objects/utils.tsx b/x-pack/plugins/uptime/e2e/page_objects/utils.tsx index 072d4497e856d..024609e2f69ef 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/utils.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/utils.tsx @@ -38,6 +38,10 @@ export function utilsPageProvider({ page }: { page: Page }) { await page.selectOption(`[data-test-subj=${dataTestSubj}]`, value); }, + async checkByTestSubj(dataTestSubj: string, value: string) { + await page.check(`[data-test-subj=${dataTestSubj}]`); + }, + async clickByTestSubj(dataTestSubj: string) { await page.click(`[data-test-subj=${dataTestSubj}]`); }, diff --git a/x-pack/plugins/uptime/e2e/playwright_start.ts b/x-pack/plugins/uptime/e2e/playwright_start.ts index cb616be66b7c9..9108412405602 100644 --- a/x-pack/plugins/uptime/e2e/playwright_start.ts +++ b/x-pack/plugins/uptime/e2e/playwright_start.ts @@ -15,26 +15,12 @@ import './journeys'; import { createApmAndObsUsersAndRoles } from '../../apm/scripts/create_apm_users_and_roles/create_apm_users_and_roles'; import { importMonitors } from './tasks/import_monitors'; -const listOfJourneys = [ - 'uptime', - 'StepsDuration', - 'TlsFlyoutInAlertingApp', - 'StatusFlyoutInAlertingApp', - 'DefaultEmailSettings', - 'MonitorManagement-http', - 'MonitorManagement-tcp', - 'MonitorManagement-icmp', - 'MonitorManagement-browser', - 'MonitorManagement breadcrumbs', - 'Monitor Management read only user', -] as const; - export function playwrightRunTests({ headless, match }: { headless: boolean; match?: string }) { return async ({ getService }: any) => { - const result = await playwrightStart(getService, headless, match); + const results = await playwrightStart(getService, headless, match); - listOfJourneys.forEach((journey) => { - if (result?.[journey] && result[journey].status !== 'succeeded') { + Object.entries(results).forEach(([_journey, result]) => { + if (result.status !== 'succeeded') { throw new Error('Tests failed'); } }); @@ -66,7 +52,7 @@ async function playwrightStart(getService: any, headless = true, match?: string) }); const res = await playwrightRun({ - params: { kibanaUrl }, + params: { kibanaUrl, getService }, playwrightOptions: { headless, chromiumSandbox: false, timeout: 60 * 1000 }, match: match === 'undefined' ? '' : match, }); diff --git a/x-pack/plugins/uptime/e2e/tasks/read_kibana_config.ts b/x-pack/plugins/uptime/e2e/tasks/read_kibana_config.ts new file mode 100644 index 0000000000000..3867386e41104 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/tasks/read_kibana_config.ts @@ -0,0 +1,22 @@ +/* + * 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 path from 'path'; +import fs from 'fs'; +import yaml from 'js-yaml'; + +export type KibanaConfig = ReturnType; + +export const readKibanaConfig = () => { + const kibanaConfigDir = path.join(__filename, '../../../../../../config'); + const kibanaDevConfig = path.join(kibanaConfigDir, 'kibana.dev.yml'); + const kibanaConfig = path.join(kibanaConfigDir, 'kibana.yml'); + + return (yaml.safeLoad( + fs.readFileSync(fs.existsSync(kibanaDevConfig) ? kibanaDevConfig : kibanaConfig, 'utf8') + ) || {}) as Record; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx index df0abcb88180b..1176ea5e6a5c3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx @@ -75,7 +75,7 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro const button = ( setIsPopOverOpen(true) : onEnableJob} + onClick={hasMLJob ? () => setIsPopOverOpen(!isPopOverOpen) : onEnableJob} disabled={hasMLJob && !canDeleteMLJob} isLoading={showLoading} size="s" diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_config/locations.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_config/locations.test.tsx index 11caf092c93c7..ccc3e7b619c68 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_config/locations.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_config/locations.test.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { render } from '../../../lib/helper/rtl_helpers'; -import { ServiceLocations, LOCATIONS_LABEL } from './locations'; -import userEvent from '@testing-library/user-event'; +import { ServiceLocations } from './locations'; describe('', () => { const setLocations = jest.fn(); @@ -52,47 +51,7 @@ describe('', () => { { state } ); - expect(screen.getByText(LOCATIONS_LABEL)).toBeInTheDocument(); - expect(screen.queryByText('US Central')).not.toBeInTheDocument(); - }); - - it('shows location options when clicked', async () => { - render( - , - { state } - ); - - userEvent.click(screen.getByRole('button')); - - expect(screen.getByText('US Central')).toBeInTheDocument(); - }); - - it('prevents bad inputs', async () => { - render( - , - { state } - ); - - userEvent.click(screen.getByRole('button')); - userEvent.type(screen.getByRole('textbox'), 'fake location'); - - expect(screen.getByText("doesn't match any options")).toBeInTheDocument(); - - userEvent.keyboard(`{enter}`); - - expect(screen.getByText('"fake location" is not a valid option')).toBeInTheDocument(); - }); - - it('calls setLocations', async () => { - render( - , - { state } - ); - - userEvent.click(screen.getByRole('button')); - userEvent.click(screen.getByText('US Central')); - - expect(setLocations).toBeCalledWith([location]); + expect(screen.queryByText('US Central')).toBeInTheDocument(); }); it('shows invalid error', async () => { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_config/locations.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_config/locations.tsx index 2d261e169299a..1877cc4ade126 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_config/locations.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_config/locations.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; +import { EuiCheckboxGroup, EuiFormRow } from '@elastic/eui'; import { monitorManagementListSelector } from '../../../state/selectors'; import { ServiceLocation } from '../../../../common/runtime_types'; @@ -20,51 +20,49 @@ interface Props { export const ServiceLocations = ({ selectedLocations, setLocations, isInvalid }: Props) => { const [error, setError] = useState(null); + const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState>( + {} + ); const { locations } = useSelector(monitorManagementListSelector); - const onLocationChange = ( - selectedLocationOptions: Array> - ) => { - setLocations(selectedLocationOptions as ServiceLocation[]); - setError(null); - }; - - const onSearchChange = (value: string, hasMatchingOptions?: boolean) => { - setError(value.length === 0 || hasMatchingOptions ? null : getInvalidOptionError(value)); - }; - - const onBlur = (event: unknown) => { - const inputElement = (event as FocusEvent)?.target as HTMLInputElement; - if (inputElement) { - const { value } = inputElement; - setError(value.length === 0 ? null : getInvalidOptionError(value)); + const onLocationChange = (optionId: string) => { + const isSelected = !checkboxIdToSelectedMap[optionId]; + const location = locations.find((loc) => loc.id === optionId); + if (isSelected) { + setLocations((prevLocations) => (location ? [...prevLocations, location] : prevLocations)); + } else { + setLocations((prevLocations) => [...prevLocations].filter((loc) => loc.id !== optionId)); } + setError(null); }; const errorMessage = error ?? (isInvalid ? VALIDATION_ERROR : null); + useEffect(() => { + const newCheckboxIdToSelectedMap = selectedLocations.reduce>( + (acc, location) => { + acc[location.id] = true; + return acc; + }, + {} + ); + setCheckboxIdToSelectedMap(newCheckboxIdToSelectedMap); + }, [selectedLocations]); + return ( - ({ + ...location, + 'data-test-subj': `syntheticsServiceLocation--${location.id}`, + }))} + idToSelectedMap={checkboxIdToSelectedMap} + onChange={(id) => onLocationChange(id)} /> ); }; -const PLACEHOLDER_LABEL = i18n.translate( - 'xpack.uptime.monitorManagement.serviceLocationsPlaceholderLabel', - { - defaultMessage: 'Select one or more locations to run your monitor.', - } -); - const VALIDATION_ERROR = i18n.translate( 'xpack.uptime.monitorManagement.serviceLocationsValidationError', { @@ -72,14 +70,6 @@ const VALIDATION_ERROR = i18n.translate( } ); -const getInvalidOptionError = (value: string) => - i18n.translate('xpack.uptime.monitorManagement.serviceLocationsOptionError', { - defaultMessage: '"{value}" is not a valid option', - values: { - value, - }, - }); - export const LOCATIONS_LABEL = i18n.translate( 'xpack.uptime.monitorManagement.monitorLocationsLabel', { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx index fd868214d4db9..e79150a4eb79e 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx @@ -7,19 +7,12 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import { render } from '../../../lib/helper/rtl_helpers'; -import * as fetchers from '../../../state/api/monitor_management'; -import { - FETCH_STATUS, - useFetcher as originalUseFetcher, -} from '../../../../../observability/public'; -import { spyOnUseFetcher } from '../../../lib/helper/spy_use_fetcher'; + import { Actions } from './actions'; describe('', () => { const onUpdate = jest.fn(); - const useFetcher = spyOnUseFetcher({}); it('navigates to edit monitor flow on edit pencil', () => { render(); @@ -29,38 +22,4 @@ describe('', () => { '/app/uptime/edit-monitor/dGVzdC1pZA==' ); }); - - it('calls delete monitor on monitor deletion', () => { - useFetcher.mockImplementation(originalUseFetcher); - const deleteMonitor = jest.spyOn(fetchers, 'deleteMonitor'); - const id = 'test-id'; - render(); - - expect(deleteMonitor).not.toBeCalled(); - - userEvent.click(screen.getByRole('button')); - - expect(deleteMonitor).toBeCalledWith({ id }); - }); - - it('calls set refresh when deletion is successful', () => { - const id = 'test-id'; - render(); - - userEvent.click(screen.getByLabelText('Delete monitor')); - - expect(onUpdate).toHaveBeenCalled(); - }); - - it('shows loading spinner while waiting for monitor to delete', () => { - const id = 'test-id'; - useFetcher.mockReturnValue({ - data: {}, - status: FETCH_STATUS.LOADING, - refetch: () => {}, - }); - render(); - - expect(screen.getByLabelText('Deleting monitor...')).toBeInTheDocument(); - }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx index 6c85ae3d8a2a0..5fa29b7cd7c56 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx @@ -5,13 +5,11 @@ * 2.0. */ -import React, { useContext, useState, useEffect } from 'react'; +import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiFlexItem, EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiButtonIcon, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { UptimeSettingsContext } from '../../../contexts'; -import { useFetcher, FETCH_STATUS } from '../../../../../observability/public'; -import { deleteMonitor } from '../../../state/api'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { DeleteMonitor } from './delete_monitor'; interface Props { id: string; @@ -20,41 +18,8 @@ interface Props { } export const Actions = ({ id, onUpdate, isDisabled }: Props) => { - const [isDeleting, setIsDeleting] = useState(false); const { basePath } = useContext(UptimeSettingsContext); - const { notifications } = useKibana(); - - const { status } = useFetcher(() => { - if (isDeleting) { - return deleteMonitor({ id }); - } - }, [id, isDeleting]); - - // TODO: add popup to confirm deletion - const handleDelete = () => { - setIsDeleting(true); - }; - - useEffect(() => { - if (status === FETCH_STATUS.SUCCESS || status === FETCH_STATUS.FAILURE) { - setIsDeleting(false); - } - if (status === FETCH_STATUS.FAILURE) { - notifications.toasts.danger({ - title:

{MONITOR_DELETE_FAILURE_LABEL}

, - toastLifeTimeMs: 3000, - }); - } else if (status === FETCH_STATUS.SUCCESS) { - onUpdate(); - notifications.toasts.success({ - title:

{MONITOR_DELETE_SUCCESS_LABEL}

, - toastLifeTimeMs: 3000, - }); - } - }, [setIsDeleting, onUpdate, notifications.toasts, status]); - - // TODO: Add popovers to icons return ( @@ -67,17 +32,7 @@ export const Actions = ({ id, onUpdate, isDisabled }: Props) => { /> - {status === FETCH_STATUS.LOADING ? ( - - ) : ( - - )} + ); @@ -86,29 +41,3 @@ export const Actions = ({ id, onUpdate, isDisabled }: Props) => { const EDIT_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.editMonitorLabel', { defaultMessage: 'Edit monitor', }); - -const DELETE_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.deleteMonitorLabel', { - defaultMessage: 'Delete monitor', -}); - -const MONITOR_DELETE_SUCCESS_LABEL = i18n.translate( - 'xpack.uptime.monitorManagement.monitorDeleteSuccessMessage', - { - defaultMessage: 'Monitor deleted successfully.', - } -); - -// TODO: Discuss error states with product -const MONITOR_DELETE_FAILURE_LABEL = i18n.translate( - 'xpack.uptime.monitorManagement.monitorDeleteFailureMessage', - { - defaultMessage: 'Monitor was unable to be deleted. Please try again later.', - } -); - -const MONITOR_DELETE_LOADING_LABEL = i18n.translate( - 'xpack.uptime.monitorManagement.monitorDeleteLoadingMessage', - { - defaultMessage: 'Deleting monitor...', - } -); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx new file mode 100644 index 0000000000000..a5c712b6e0456 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx @@ -0,0 +1,60 @@ +/* + * 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/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../lib/helper/rtl_helpers'; +import * as fetchers from '../../../state/api/monitor_management'; +import { + FETCH_STATUS, + useFetcher as originalUseFetcher, +} from '../../../../../observability/public'; +import { spyOnUseFetcher } from '../../../lib/helper/spy_use_fetcher'; +import { Actions } from './actions'; +import { DeleteMonitor } from './delete_monitor'; + +describe('', () => { + const onUpdate = jest.fn(); + const useFetcher = spyOnUseFetcher({}); + + it('calls delete monitor on monitor deletion', () => { + useFetcher.mockImplementation(originalUseFetcher); + const deleteMonitor = jest.spyOn(fetchers, 'deleteMonitor'); + const id = 'test-id'; + render(); + + expect(deleteMonitor).not.toBeCalled(); + + userEvent.click(screen.getByRole('button')); + + userEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + expect(deleteMonitor).toBeCalledWith({ id }); + }); + + it('calls set refresh when deletion is successful', () => { + const id = 'test-id'; + render(); + + userEvent.click(screen.getByLabelText('Delete monitor')); + + expect(onUpdate).toHaveBeenCalled(); + }); + + it('shows loading spinner while waiting for monitor to delete', () => { + const id = 'test-id'; + useFetcher.mockReturnValue({ + data: {}, + status: FETCH_STATUS.LOADING, + refetch: () => {}, + }); + render(); + + expect(screen.getByLabelText('Deleting monitor...')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.tsx new file mode 100644 index 0000000000000..e0b706241b79a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.tsx @@ -0,0 +1,142 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { useEffect, useState } from 'react'; +import { EuiButtonIcon, EuiConfirmModal, EuiLoadingSpinner } from '@elastic/eui'; + +import { FETCH_STATUS, useFetcher } from '../../../../../observability/public'; +import { deleteMonitor } from '../../../state/api'; +import { kibanaService } from '../../../state/kibana_service'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; + +export const DeleteMonitor = ({ + id, + onUpdate, + isDisabled, +}: { + id: string; + isDisabled?: boolean; + onUpdate: () => void; +}) => { + const [isDeleting, setIsDeleting] = useState(false); + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + + const onConfirmDelete = () => { + setIsDeleting(true); + setIsDeleteModalVisible(false); + }; + const showDeleteModal = () => setIsDeleteModalVisible(true); + + const { status } = useFetcher(() => { + if (isDeleting) { + return deleteMonitor({ id }); + } + }, [id, isDeleting]); + + const handleDelete = () => { + showDeleteModal(); + }; + + useEffect(() => { + if (status === FETCH_STATUS.SUCCESS || status === FETCH_STATUS.FAILURE) { + setIsDeleting(false); + } + if (status === FETCH_STATUS.FAILURE) { + kibanaService.toasts.addDanger( + { + title: toMountPoint( +

{MONITOR_DELETE_FAILURE_LABEL}

+ ), + }, + { toastLifeTimeMs: 3000 } + ); + } else if (status === FETCH_STATUS.SUCCESS) { + onUpdate(); + kibanaService.toasts.addSuccess( + { + title: toMountPoint( +

{MONITOR_DELETE_SUCCESS_LABEL}

+ ), + }, + { toastLifeTimeMs: 3000 } + ); + } + }, [setIsDeleting, onUpdate, status]); + + const destroyModal = ( + setIsDeleteModalVisible(false)} + onConfirm={onConfirmDelete} + cancelButtonText={NO_LABEL} + confirmButtonText={YES_LABEL} + buttonColor="danger" + defaultFocusedButton="confirm" + > +

{DELETE_DESCRIPTION_LABEL}

+
+ ); + + return ( + <> + {status === FETCH_STATUS.LOADING ? ( + + ) : ( + + )} + {isDeleteModalVisible && destroyModal} + + ); +}; + +const DELETE_DESCRIPTION_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.confirmDescriptionLabel', + { + defaultMessage: 'Are you sure you want to do delete the monitor?', + } +); + +const YES_LABEL = i18n.translate('xpack.uptime.monitorManagement.yesLabel', { + defaultMessage: 'Yes', +}); + +const NO_LABEL = i18n.translate('xpack.uptime.monitorManagement.noLabel', { + defaultMessage: 'No', +}); + +const DELETE_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.deleteMonitorLabel', { + defaultMessage: 'Delete monitor', +}); + +const MONITOR_DELETE_SUCCESS_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.monitorDeleteSuccessMessage', + { + defaultMessage: 'Monitor deleted successfully.', + } +); + +// TODO: Discuss error states with product +const MONITOR_DELETE_FAILURE_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.monitorDeleteFailureMessage', + { + defaultMessage: 'Monitor was unable to be deleted. Please try again later.', + } +); + +const MONITOR_DELETE_LOADING_LABEL = i18n.translate( + 'xpack.uptime.monitorManagement.monitorDeleteLoadingMessage', + { + defaultMessage: 'Deleting monitor...', + } +); diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts index a58c4dd421cbe..ffec06259f4e7 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts @@ -16,7 +16,7 @@ import { UptimeServerSetup } from '../adapters/framework'; export async function getServiceLocations(server: UptimeServerSetup) { const locations: ServiceLocations = []; - if (!server.config.service!.manifestUrl!) { + if (!server.config.service?.manifestUrl) { return { locations }; } diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts index 4d92d2e2c76df..20d6e57a71c12 100644 --- a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts +++ b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./privileges')); loadTestFile(require.resolve('./es_deprecations')); loadTestFile(require.resolve('./es_deprecation_logs')); + loadTestFile(require.resolve('./remote_clusters')); }); } diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/remote_clusters.ts b/x-pack/test/api_integration/apis/upgrade_assistant/remote_clusters.ts new file mode 100644 index 0000000000000..5d8dcaf339068 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/remote_clusters.ts @@ -0,0 +1,75 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { API_BASE_PATH } from '../../../../plugins/upgrade_assistant/common/constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const log = getService('log'); + + describe('Remote clusters', () => { + describe('GET /api/upgrade_assistant/remote_clusters', () => { + before(async () => { + try { + // Configure a remote cluster + await es.cluster.putSettings({ + body: { + persistent: { + cluster: { + remote: { + test_cluster: { + seeds: ['127.0.0.1:9400'], + }, + }, + }, + }, + }, + }); + } catch (e) { + log.debug('Error creating remote cluster'); + throw e; + } + }); + + after(async () => { + try { + // Delete remote cluster + await es.cluster.putSettings({ + body: { + persistent: { + cluster: { + remote: { + test_cluster: { + seeds: null, + }, + }, + }, + }, + }, + }); + } catch (e) { + log.debug('Error deleting remote cluster'); + throw e; + } + }); + + it('returns an array of remote clusters', async () => { + const { body: apiRequestResponse } = await supertest + .get(`${API_BASE_PATH}/remote_clusters`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(Array.isArray(apiRequestResponse)).be(true); + expect(apiRequestResponse.length).be(1); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index aa2a61f8b0092..af355695f3ed8 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -37,7 +37,6 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi '--xpack.ruleRegistry.write.cache.enabled=false', '--xpack.uptime.ui.monitorManagement.enabled=true', '--xpack.uptime.service.password=test', - '--xpack.uptime.service.manifestUrl=http://test.com', '--xpack.uptime.service.username=localKibanaIntegrationTestsUser', `--xpack.securitySolution.enableExperimental=${JSON.stringify(['ruleRegistryEnabled'])}`, ], diff --git a/x-pack/test/cases_api_integration/common/lib/utils.ts b/x-pack/test/cases_api_integration/common/lib/utils.ts index de27836a9559e..9c56db80e45fc 100644 --- a/x-pack/test/cases_api_integration/common/lib/utils.ts +++ b/x-pack/test/cases_api_integration/common/lib/utils.ts @@ -1131,3 +1131,15 @@ export const getServiceNowSimulationServer = async (): Promise<{ return { server, url }; }; + +/** + * Extracts the warning value a warning header that is formatted according to RFC 7234. + * For example for the string 299 Kibana-8.1.0 "Deprecation endpoint", the return value is Deprecation endpoint. + * + */ +export const extractWarningValueFromWarningHeader = (warningHeader: string) => { + const firstQuote = warningHeader.indexOf('"'); + const lastQuote = warningHeader.length - 1; + const warningValue = warningHeader.substring(firstQuote + 1, lastQuote); + return warningValue; +}; diff --git a/x-pack/test/cases_api_integration/common/lib/validation.ts b/x-pack/test/cases_api_integration/common/lib/validation.ts index 220074f22db86..826ac7e2600e1 100644 --- a/x-pack/test/cases_api_integration/common/lib/validation.ts +++ b/x-pack/test/cases_api_integration/common/lib/validation.ts @@ -37,3 +37,17 @@ export function arraysToEqual(array1?: object[], array2?: object[]) { const array1AsSet = new Set(array1); return array2.every((item) => array1AsSet.has(item)); } + +/** + * Regular expression to test if a string matches the RFC7234 specification (without warn-date) for warning headers. This pattern assumes that the warn code + * is always 299. Further, this pattern assumes that the warn agent represents a version of Kibana. + * + * Example: 299 Kibana-8.2.0 "Deprecated endpoint" + */ +const WARNING_HEADER_REGEX = + /299 Kibana-\d+.\d+.\d+(?:-(?:alpha|beta|rc)\\d+)?(?:-SNAPSHOT)? \".+\"/g; + +export const assertWarningHeader = (warningHeader: string) => { + const res = warningHeader.match(WARNING_HEADER_REGEX); + expect(res).not.to.be(null); +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/get_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/get_case.ts index 8df044baf813e..abafc514e56f3 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/get_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/get_case.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; +import { AttributesTypeUser, getCaseDetailsUrl } from '../../../../../../plugins/cases/common/api'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { defaultUser, @@ -24,6 +24,7 @@ import { createComment, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromSavedObject, + extractWarningValueFromWarningHeader, } from '../../../../common/lib/utils'; import { secOnly, @@ -37,6 +38,7 @@ import { obsSec, } from '../../../../common/lib/authentication/users'; import { getUserInfo } from '../../../../common/lib/authentication'; +import { assertWarningHeader } from '../../../../common/lib/validation'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -191,5 +193,29 @@ export default ({ getService }: FtrProviderContext): void => { }); }); }); + + describe('deprecations', () => { + for (const paramValue of [true, false]) { + it(`should return a warning header if includeComments=${paramValue}`, async () => { + const theCase = await createCase(supertest, postCaseReq); + const res = await supertest + .get(`${getCaseDetailsUrl(theCase.id)}?includeComments=${paramValue}`) + .expect(200); + const warningHeader = res.header.warning; + + assertWarningHeader(warningHeader); + + const warningValue = extractWarningValueFromWarningHeader(warningHeader); + expect(warningValue).to.be('Deprecated query parameter includeComments'); + }); + } + + it('should not return a warning header if includeComments is not provided', async () => { + const theCase = await createCase(supertest, postCaseReq); + const res = await supertest.get(getCaseDetailsUrl(theCase.id)).expect(200); + const warningHeader = res.header.warning; + expect(warningHeader).to.be(undefined); + }); + }); }); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index 02ace7077a20a..c170dc0ff3ccd 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -16,6 +16,7 @@ import { getAllCasesStatuses, deleteAllCaseItems, superUserSpace1Auth, + extractWarningValueFromWarningHeader, } from '../../../../../common/lib/utils'; import { globalRead, @@ -26,6 +27,8 @@ import { secOnlyRead, superUser, } from '../../../../../common/lib/authentication/users'; +import { CASE_STATUS_URL } from '../../../../../../../plugins/cases/common/constants'; +import { assertWarningHeader } from '../../../../../common/lib/validation'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -181,5 +184,18 @@ export default ({ getService }: FtrProviderContext): void => { }); } }); + + describe('deprecations', () => { + it('should return a warning header', async () => { + await createCase(supertest, postCaseReq); + const res = await supertest.get(CASE_STATUS_URL).expect(200); + const warningHeader = res.header.warning; + + assertWarningHeader(warningHeader); + + const warningValue = extractWarningValueFromWarningHeader(warningHeader); + expect(warningValue).to.be('Deprecated endpoint'); + }); + }); }); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index e0ee2576ef610..d6e52b6577fef 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -15,6 +15,7 @@ import { createComment, getAllComments, superUserSpace1Auth, + extractWarningValueFromWarningHeader, } from '../../../../common/lib/utils'; import { globalRead, @@ -27,6 +28,8 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { getCaseCommentsUrl } from '../../../../../../plugins/cases/common/api'; +import { assertWarningHeader } from '../../../../common/lib/validation'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -148,5 +151,18 @@ export default ({ getService }: FtrProviderContext): void => { }); }); }); + + describe('deprecations', () => { + it('should return a warning header', async () => { + const theCase = await createCase(supertest, postCaseReq); + const res = await supertest.get(getCaseCommentsUrl(theCase.id)).expect(200); + const warningHeader = res.header.warning; + + assertWarningHeader(warningHeader); + + const warningValue = extractWarningValueFromWarningHeader(warningHeader); + expect(warningValue).to.be('Deprecated endpoint'); + }); + }); }); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 17863b282bf05..b09059966a16b 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -13,6 +13,7 @@ import { CaseStatuses, CommentType, ConnectorTypes, + getCaseUserActionUrl, } from '../../../../../../plugins/cases/common/api'; import { CreateCaseUserAction } from '../../../../../../plugins/cases/common/api/cases/user_actions/create_case'; import { postCaseReq, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; @@ -26,6 +27,7 @@ import { createComment, updateComment, deleteComment, + extractWarningValueFromWarningHeader, } from '../../../../common/lib/utils'; import { globalRead, @@ -36,6 +38,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { assertWarningHeader } from '../../../../common/lib/validation'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -354,5 +357,18 @@ export default ({ getService }: FtrProviderContext): void => { }); } }); + + describe('deprecations', () => { + it('should return a warning header', async () => { + const theCase = await createCase(supertest, postCaseReq); + const res = await supertest.get(getCaseUserActionUrl(theCase.id)).expect(200); + const warningHeader = res.header.warning; + + assertWarningHeader(warningHeader); + + const warningValue = extractWarningValueFromWarningHeader(warningHeader); + expect(warningValue).to.be('Deprecated endpoint'); + }); + }); }); }; diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index d36f8124599fe..6ecf87f88b8be 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -61,10 +61,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./certificates')); }); - describe('with generated data but no data reset', () => { - loadTestFile(require.resolve('./ping_redirects')); - }); - describe('with real-world data', () => { before(async () => { await esArchiver.unload(ARCHIVE); @@ -75,7 +71,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { after(async () => await esArchiver.unload(ARCHIVE)); loadTestFile(require.resolve('./overview')); - loadTestFile(require.resolve('./monitor')); loadTestFile(require.resolve('./ml_anomaly')); loadTestFile(require.resolve('./feature_controls')); }); diff --git a/x-pack/test/functional/apps/uptime/monitor.ts b/x-pack/test/functional/apps/uptime/monitor.ts deleted file mode 100644 index de88bd12a00fb..0000000000000 --- a/x-pack/test/functional/apps/uptime/monitor.ts +++ /dev/null @@ -1,60 +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 { FtrProviderContext } from '../../ftr_provider_context'; - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const uptimeService = getService('uptime'); - const { uptime } = getPageObjects(['uptime']); - const archive = 'x-pack/test/functional/es_archives/uptime/full_heartbeat'; - - describe('monitor page', function () { - this.tags(['skipFirefox']); - const dateStart = 'Sep 10, 2019 @ 12:40:08.078'; - const dateEnd = 'Sep 11, 2019 @ 19:40:08.078'; - const monitorId = '0000-intermittent'; - - before(async () => { - await esArchiver.loadIfNeeded(archive); - await uptimeService.navigation.goToUptime(); - }); - - after(async () => { - await esArchiver.unload(archive); - }); - - describe('navigation to monitor page', () => { - before(async () => { - await uptime.loadDataAndGoToMonitorPage(dateStart, dateEnd, monitorId); - }); - - it('should select the ping list location filter', async () => { - await uptimeService.common.selectFilterItem('Location', 'mpls'); - }); - - it('should set the status filter', async () => { - await uptimeService.common.setStatusFilterUp(); - }); - - it('displays ping data as expected', async () => { - await uptime.checkPingListInteractions([ - 'XZtoHm0B0I9WX_CznN-6', - '7ZtoHm0B0I9WX_CzJ96M', - 'pptnHm0B0I9WX_Czst5X', - 'I5tnHm0B0I9WX_CzPd46', - 'y5tmHm0B0I9WX_Czx93x', - 'XZtmHm0B0I9WX_CzUt3H', - '-JtlHm0B0I9WX_Cz3dyX', - 'k5tlHm0B0I9WX_CzaNxm', - 'NZtkHm0B0I9WX_Cz89w9', - 'zJtkHm0B0I9WX_CzftsN', - ]); - }); - }); - }); -}; diff --git a/x-pack/test/functional/apps/uptime/ping_redirects.ts b/x-pack/test/functional/apps/uptime/ping_redirects.ts deleted file mode 100644 index 06352d37ada28..0000000000000 --- a/x-pack/test/functional/apps/uptime/ping_redirects.ts +++ /dev/null @@ -1,76 +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 { makeChecksWithStatus } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - const { uptime: uptimePage, header } = getPageObjects(['uptime', 'header']); - const uptime = getService('uptime'); - const esArchiver = getService('esArchiver'); - - const archive = 'x-pack/test/functional/es_archives/uptime/blank'; - - const monitor = () => uptime.monitor; - - // FLAKY: https://github.com/elastic/kibana/issues/118476 - describe.skip('Ping redirects', () => { - const start = '~ 15 minutes ago'; - const end = 'now'; - - const MONITOR_ID = 'redirect-testing-id'; - - before(async () => { - await esArchiver.loadIfNeeded(archive); - }); - - after('unload', async () => { - await esArchiver.unload(archive); - }); - - beforeEach(async () => { - await makeChecksWithStatus( - getService('es'), - MONITOR_ID, - 5, - 2, - 10000, - { - http: { - rtt: { total: { us: 157784 } }, - response: { - status_code: 200, - redirects: ['http://localhost:3000/first', 'https://www.washingtonpost.com/'], - body: { - bytes: 642102, - hash: '597a8cfb33ff8e09bff16283306553c3895282aaf5386e1843d466d44979e28a', - }, - }, - }, - }, - 'up' - ); - await delay(1000); - }); - - it('loads and goes to details page', async () => { - await uptime.navigation.goToUptime(); - await uptimePage.loadDataAndGoToMonitorPage(start, end, MONITOR_ID); - }); - - it('display redirect info in detail panel', async () => { - await header.waitUntilLoadingHasFinished(); - await monitor().hasRedirectInfo(); - }); - - it('displays redirects in ping list expand row', async () => { - await monitor().hasRedirectInfoInPingList(); - }); - }); -}; diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts deleted file mode 100644 index 6f65c620b527c..0000000000000 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts +++ /dev/null @@ -1,134 +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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - // FLAKY: https://github.com/elastic/kibana/issues/111667 - describe.skip('uptime anomaly alert', () => { - const pageObjects = getPageObjects(['common', 'uptime']); - const supertest = getService('supertest'); - const retry = getService('retry'); - - const monitorId = '0000-intermittent'; - - const uptime = getService('uptime'); - - const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; - const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; - let alerts: any; - const alertId = 'uptime-anomaly-alert'; - - before(async () => { - alerts = getService('uptime').alerts; - - await uptime.navigation.goToUptime(); - - await uptime.navigation.loadDataAndGoToMonitorPage( - DEFAULT_DATE_START, - DEFAULT_DATE_END, - monitorId - ); - }); - - it('can delete existing job', async () => { - if (await uptime.ml.alreadyHasJob()) { - await uptime.ml.openMLManageMenu(); - await uptime.ml.deleteMLJob(); - await uptime.navigation.refreshApp(); - } - }); - - it('can open ml flyout', async () => { - await uptime.ml.openMLFlyout(); - }); - - it('has permission to create job', async () => { - expect(uptime.ml.canCreateJob()).to.eql(true); - expect(uptime.ml.hasNoLicenseInfo()).to.eql(false); - }); - - it('can create job successfully', async () => { - await uptime.ml.createMLJob(); - await pageObjects.common.closeToast(); - await uptime.ml.cancelAlertFlyout(); - }); - - it('can open ML Manage Menu', async () => { - await uptime.ml.openMLManageMenu(); - }); - - it('can open anomaly alert flyout', async () => { - await uptime.ml.openAlertFlyout(); - }); - - it('can set alert name', async () => { - await alerts.setAlertName(alertId); - }); - - it('can set alert tags', async () => { - await alerts.setAlertTags(['uptime', 'anomaly-alert']); - }); - - it('can change anomaly alert threshold', async () => { - await uptime.ml.changeAlertThreshold('major'); - }); - - it('can save alert', async () => { - await alerts.clickSaveAlertButton(); - await alerts.clickSaveAlertsConfirmButton(); - await pageObjects.common.closeToast(); - }); - - it('has created a valid alert with expected parameters', async () => { - let alert: any; - await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`); - const alertsFromThisTest = apiResponse.body.data.filter( - ({ name }: { name: string }) => name === alertId - ); - expect(alertsFromThisTest).to.have.length(1); - alert = alertsFromThisTest[0]; - }); - - // Ensure the parameters and other stateful data - // on the alert match up with the values we provided - // for our test helper to input into the flyout. - const { actions, alertTypeId, consumer, id, params, tags } = alert; - try { - expect(actions).to.eql([]); - expect(alertTypeId).to.eql('xpack.uptime.alerts.durationAnomaly'); - expect(consumer).to.eql('uptime'); - expect(tags).to.eql(['uptime', 'anomaly-alert']); - expect(params.monitorId).to.eql(monitorId); - expect(params.severity).to.eql(50); - } finally { - await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); - } - }); - - it('change button to disable anomaly alert', async () => { - await uptime.ml.openMLManageMenu(); - expect(uptime.ml.manageAnomalyAlertIsVisible()).to.eql(true); - }); - - it('can delete job successfully', async () => { - await uptime.ml.deleteMLJob(); - }); - - it('verifies that alert is also deleted', async () => { - await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`); - const alertsFromThisTest = apiResponse.body.data.filter( - ({ name }: { name: string }) => name === alertId - ); - expect(alertsFromThisTest).to.have.length(0); - }); - }); - }); -}; diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts index 222dcd22d6f86..d2078267bde85 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts @@ -24,7 +24,6 @@ export default ({ getService, loadTestFile }: FtrProviderContext) => { after(async () => await esArchiver.unload(ARCHIVE)); loadTestFile(require.resolve('./alert_flyout')); - loadTestFile(require.resolve('./anomaly_alert')); loadTestFile(require.resolve('./simple_down_alert')); }); });