diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 555d479d860e1d..4c0c335b3c33eb 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -344,4 +344,24 @@ The output directory after extracting an archive no longer includes the target p *Impact:* Configuration management tools and automation will need to be updated to use the new directory. +[float] +=== `elasticsearch.preserveHost` is no longer valid +*Details:* The deprecated `elasticsearch.preserveHost` setting in the `kibana.yml` file has been removed. + +*Impact:* Configure {kibana-ref}/settings.html#elasticsearch-requestHeadersWhitelist[`elasticsearch.requestHeadersWhitelist`] to whitelist client-side headers. + +[float] +=== `elasticsearch.startupTimeout` is no longer valid +*Details:* The deprecated `elasticsearch.startupTimeout` setting in the `kibana.yml` file has been removed. + +*Impact:* Kibana will keep on trying to connect to Elasticsearch until it manages to connect. + +[float] +=== `savedObjects.indexCheckTimeout` is no longer valid +*Details:* The deprecated `savedObjects.indexCheckTimeout` setting in the `kibana.yml` file has been removed. + +[float] +=== `server.xsrf.token` is no longer valid +*Details:* The deprecated `server.xsrf.token` setting in the `kibana.yml` file has been removed. + // end::notable-breaking-changes[] diff --git a/package.json b/package.json index 8f329b4c548861..c2b30ef7ef1509 100644 --- a/package.json +++ b/package.json @@ -96,9 +96,9 @@ "@elastic/apm-rum-react": "^1.3.1", "@elastic/charts": "34.2.1", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.20", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.21", "@elastic/ems-client": "7.15.0", - "@elastic/eui": "37.6.0", + "@elastic/eui": "38.0.1", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/maki": "6.3.0", @@ -266,6 +266,7 @@ "js-levenshtein": "^1.1.6", "js-search": "^1.4.3", "js-sha256": "^0.9.0", + "js-sql-parser": "^1.4.1", "js-yaml": "^3.14.0", "json-stable-stringify": "^1.0.1", "json-stringify-pretty-compact": "1.2.0", @@ -298,7 +299,6 @@ "nock": "12.0.3", "node-fetch": "^2.6.1", "node-forge": "^0.10.0", - "node-sql-parser": "^3.6.1", "nodemailer": "^6.6.2", "normalize-path": "^3.0.0", "object-hash": "^1.3.1", diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 4450533090c7fc..e73d5e8002a029 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -4267,7 +4267,7 @@ exports[`Header renders 1`] = ` } button={ } data-test-subj="collapsibleNav" - id="mockId" + id="generated-id" isOpen={false} onClose={[Function]} ownFocus={false} size={248} > - -
-
    -
  • - -
  • -
  • - -
  • - -
  • - - ... -
  • - -
  • - -
  • - -
  • - ... - -
  • - -
  • - -
  • -
  • - -
  • -
-
- -
-
- - -
-
diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index e5244c110ad205..ac78e8cac4f074 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -24,14 +24,6 @@ export class KibanaLegacyPlugin { loadFontAwesome: async () => { await import('./font_awesome'); }, - /** - * Loads angular bootstrap modules. Should be removed once the last consumer has migrated to EUI - * @deprecated - */ - loadAngularBootstrap: async () => { - const { initAngularBootstrap } = await import('./angular_bootstrap'); - initAngularBootstrap(); - }, }; } } diff --git a/src/plugins/kibana_legacy/public/utils/index.ts b/src/plugins/kibana_legacy/public/utils/index.ts index 94233558b4627c..9bfc185b6a69e9 100644 --- a/src/plugins/kibana_legacy/public/utils/index.ts +++ b/src/plugins/kibana_legacy/public/utils/index.ts @@ -6,9 +6,5 @@ * Side Public License, v 1. */ -// @ts-ignore -export { KbnAccessibleClickProvider } from './kbn_accessible_click'; // @ts-ignore export { PrivateProvider, IPrivate } from './private'; -// @ts-ignore -export { registerListenEventListener } from './register_listen_event_listener'; diff --git a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts deleted file mode 100644 index 16adb3750e9ea6..00000000000000 --- a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Injectable, IDirectiveFactory, IScope, IAttributes, IController } from 'angular'; - -export const KbnAccessibleClickProvider: Injectable< - IDirectiveFactory ->; diff --git a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js deleted file mode 100644 index adcd133bf17193..00000000000000 --- a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js +++ /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 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 { accessibleClickKeys, keys } from '@elastic/eui'; - -export function KbnAccessibleClickProvider() { - return { - restrict: 'A', - controller: ($element) => { - $element.on('keydown', (e) => { - // Prevent a scroll from occurring if the user has hit space. - if (e.key === keys.SPACE) { - e.preventDefault(); - } - }); - }, - link: (scope, element, attrs) => { - // The whole point of this directive is to hack in functionality that native buttons provide - // by default. - const elementType = element.prop('tagName'); - - if (elementType === 'BUTTON') { - throw new Error(`kbnAccessibleClick doesn't need to be used on a button.`); - } - - if (elementType === 'A' && attrs.href !== undefined) { - throw new Error( - `kbnAccessibleClick doesn't need to be used on a link if it has a href attribute.` - ); - } - - // We're emulating a click action, so we should already have a regular click handler defined. - if (!attrs.ngClick) { - throw new Error('kbnAccessibleClick requires ng-click to be defined on its element.'); - } - - // If the developer hasn't already specified attributes required for accessibility, add them. - if (attrs.tabindex === undefined) { - element.attr('tabindex', '0'); - } - - if (attrs.role === undefined) { - element.attr('role', 'button'); - } - - element.on('keyup', (e) => { - // Support keyboard accessibility by emulating mouse click on ENTER or SPACE keypress. - if (accessibleClickKeys[e.key]) { - // Delegate to the click handler on the element (assumed to be ng-click). - element.click(); - } - }); - }, - }; -} diff --git a/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.d.ts b/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.d.ts deleted file mode 100644 index 800965baba4b4e..00000000000000 --- a/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export function registerListenEventListener($rootScope: unknown): void; diff --git a/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.js b/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.js deleted file mode 100644 index be91a69a9240df..00000000000000 --- a/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export function registerListenEventListener($rootScope) { - /** - * Helper that registers an event listener, and removes that listener when - * the $scope is destroyed. - * - * @param {EventEmitter} emitter - the event emitter to listen to - * @param {string} eventName - the event name - * @param {Function} handler - the event handler - * @return {undefined} - */ - $rootScope.constructor.prototype.$listen = function (emitter, eventName, handler) { - emitter.on(eventName, handler); - this.$on('$destroy', function () { - emitter.off(eventName, handler); - }); - }; -} diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 53eb49e1013b05..a8a391995b0051 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -408,12 +408,12 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'text', _meta: { description: 'Non-default value of setting.' }, }, - 'apm:enableSignificantTerms': { + 'observability:enableInspectEsQueries': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'observability:enableInspectEsQueries': { - type: 'boolean', + 'observability:maxSuggestions': { + type: 'integer', _meta: { description: 'Non-default value of setting.' }, }, 'banners:placement': { diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index b76ef14e62b8c3..7ea80ffb77dda3 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -34,8 +34,8 @@ export interface UsageStats { 'discover:showMultiFields': boolean; 'discover:maxDocFieldsDisplayed': number; 'securitySolution:rulesTableRefresh': string; - 'apm:enableSignificantTerms': boolean; 'observability:enableInspectEsQueries': boolean; + 'observability:maxSuggestions': number; 'visualize:enableLabs': boolean; 'visualization:heatmap:maxBuckets': number; 'visualization:colorMapping': string; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 2363c0ca103a2d..c6724056f77a54 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7629,14 +7629,14 @@ "description": "Non-default value of setting." } }, - "apm:enableSignificantTerms": { + "observability:enableInspectEsQueries": { "type": "boolean", "_meta": { "description": "Non-default value of setting." } }, - "observability:enableInspectEsQueries": { - "type": "boolean", + "observability:maxSuggestions": { + "type": "integer", "_meta": { "description": "Non-default value of setting." } @@ -9088,6 +9088,12 @@ "_meta": { "description": "Number of TSVB visualizations using \"last value\" as a time range" } + }, + "timeseries_table_use_aggregate_function": { + "type": "long", + "_meta": { + "description": "Number of TSVB table visualizations using aggregate function" + } } } }, diff --git a/src/plugins/vis_types/table/kibana.json b/src/plugins/vis_types/table/kibana.json index b3ebd5117bbc8b..a56965a2143494 100644 --- a/src/plugins/vis_types/table/kibana.json +++ b/src/plugins/vis_types/table/kibana.json @@ -6,8 +6,7 @@ "requiredPlugins": [ "expressions", "visualizations", - "data", - "kibanaLegacy" + "data" ], "requiredBundles": [ "kibanaUtils", diff --git a/src/plugins/vis_types/table/tsconfig.json b/src/plugins/vis_types/table/tsconfig.json index 9325064d571d02..578b10fb09be8e 100644 --- a/src/plugins/vis_types/table/tsconfig.json +++ b/src/plugins/vis_types/table/tsconfig.json @@ -20,7 +20,6 @@ { "path": "../../usage_collection/tsconfig.json" }, { "path": "../../expressions/tsconfig.json" }, { "path": "../../kibana_utils/tsconfig.json" }, - { "path": "../../kibana_legacy/tsconfig.json" }, { "path": "../../kibana_react/tsconfig.json" }, { "path": "../../vis_default_editor/tsconfig.json" }, { "path": "../../field_formats/tsconfig.json" } diff --git a/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.test.ts index aac6d879f48fd2..3f3a204d29263f 100644 --- a/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.test.ts @@ -49,6 +49,23 @@ const mockedSavedObject = { }), }, }, + { + attributes: { + visState: JSON.stringify({ + type: 'metrics', + title: 'TSVB visualization 4', + params: { + type: 'table', + series: [ + { + aggregate_by: 'test', + aggregate_function: 'max', + }, + ], + }, + }), + }, + }, ], } as SavedObjectsFindResponse; @@ -83,6 +100,27 @@ const mockedSavedObjectsByValue = [ }), }, }, + { + attributes: { + panelsJSON: JSON.stringify({ + type: 'visualization', + embeddableConfig: { + savedVis: { + type: 'metrics', + params: { + type: 'table', + series: [ + { + aggregate_by: 'test1', + aggregate_function: 'sum', + }, + ], + }, + }, + }, + }), + }, + }, ]; const getMockCollectorFetchContext = ( @@ -142,6 +180,58 @@ describe('Timeseries visualization usage collector', () => { expect(result).toBeUndefined(); }); + test('Returns undefined when aggregate function is null', async () => { + const mockCollectorFetchContext = getMockCollectorFetchContext({ + saved_objects: [ + { + attributes: { + panelsJSON: JSON.stringify({ + type: 'visualization', + embeddableConfig: { + savedVis: { + type: 'metrics', + params: { + type: 'table', + series: [ + { + aggregate_by: null, + aggregate_function: null, + }, + ], + }, + }, + }, + }), + }, + }, + { + attributes: { + panelsJSON: JSON.stringify({ + type: 'visualization', + embeddableConfig: { + savedVis: { + type: 'metrics', + params: { + type: 'table', + series: [ + { + axis_position: 'right', + }, + ], + }, + }, + }, + }), + }, + }, + ], + } as SavedObjectsFindResponse); + + const result = await getStats(mockCollectorFetchContext.soClient); + + expect(result).toBeUndefined(); + }); + test('Summarizes visualizations response data', async () => { const mockCollectorFetchContext = getMockCollectorFetchContext( mockedSavedObject, @@ -149,8 +239,9 @@ describe('Timeseries visualization usage collector', () => { ); const result = await getStats(mockCollectorFetchContext.soClient); - expect(result).toMatchObject({ - timeseries_use_last_value_mode_total: 3, + expect(result).toStrictEqual({ + timeseries_use_last_value_mode_total: 5, + timeseries_table_use_aggregate_function: 2, }); }); }); diff --git a/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.ts index 8309d51a9d56d9..58f0c9c7f14597 100644 --- a/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.ts @@ -15,14 +15,16 @@ import type { SavedObjectsFindResult, } from '../../../../../core/server'; import type { SavedVisState } from '../../../../visualizations/common'; +import type { Panel } from '../../common/types'; export interface TimeseriesUsage { timeseries_use_last_value_mode_total: number; + timeseries_table_use_aggregate_function: number; } const doTelemetryFoVisualizations = async ( soClient: SavedObjectsClientContract | ISavedObjectsRepository, - telemetryUseLastValueMode: (savedVis: SavedVisState) => void + calculateTelemetry: (savedVis: SavedVisState) => void ) => { const finder = await soClient.createPointInTimeFinder({ type: 'visualization', @@ -34,9 +36,9 @@ const doTelemetryFoVisualizations = async ( (response.saved_objects || []).forEach(({ attributes }: SavedObjectsFindResult) => { if (attributes?.visState) { try { - const visState: SavedVisState = JSON.parse(attributes.visState); + const visState: SavedVisState = JSON.parse(attributes.visState); - telemetryUseLastValueMode(visState); + calculateTelemetry(visState); } catch { // nothing to be here, "so" not valid } @@ -48,12 +50,12 @@ const doTelemetryFoVisualizations = async ( const doTelemetryForByValueVisualizations = async ( soClient: SavedObjectsClientContract | ISavedObjectsRepository, - telemetryUseLastValueMode: (savedVis: SavedVisState) => void + telemetryUseLastValueMode: (savedVis: SavedVisState) => void ) => { const byValueVisualizations = await findByValueEmbeddables(soClient, 'visualization'); for (const item of byValueVisualizations) { - telemetryUseLastValueMode(item.savedVis as unknown as SavedVisState); + telemetryUseLastValueMode(item.savedVis as unknown as SavedVisState); } }; @@ -62,9 +64,10 @@ export const getStats = async ( ): Promise => { const timeseriesUsage = { timeseries_use_last_value_mode_total: 0, + timeseries_table_use_aggregate_function: 0, }; - function telemetryUseLastValueMode(visState: SavedVisState) { + function telemetryUseLastValueMode(visState: SavedVisState) { if ( visState.type === 'metrics' && visState.params.type !== 'timeseries' && @@ -75,10 +78,33 @@ export const getStats = async ( } } + function telemetryTableAggFunction(visState: SavedVisState) { + if ( + visState.type === 'metrics' && + visState.params.type === 'table' && + visState.params.series && + visState.params.series.length > 0 + ) { + const usesAggregateFunction = visState.params.series.some( + (s) => s.aggregate_by && s.aggregate_function + ); + if (usesAggregateFunction) { + timeseriesUsage.timeseries_table_use_aggregate_function++; + } + } + } + await Promise.all([ + // last value usage telemetry doTelemetryFoVisualizations(soClient, telemetryUseLastValueMode), doTelemetryForByValueVisualizations(soClient, telemetryUseLastValueMode), + // table aggregate function telemetry + doTelemetryFoVisualizations(soClient, telemetryTableAggFunction), + doTelemetryForByValueVisualizations(soClient, telemetryTableAggFunction), ]); - return timeseriesUsage.timeseries_use_last_value_mode_total ? timeseriesUsage : undefined; + return timeseriesUsage.timeseries_use_last_value_mode_total || + timeseriesUsage.timeseries_table_use_aggregate_function + ? timeseriesUsage + : undefined; }; diff --git a/src/plugins/vis_types/timeseries/server/usage_collector/register_timeseries_collector.ts b/src/plugins/vis_types/timeseries/server/usage_collector/register_timeseries_collector.ts index 6fccd7ef30171d..b96d6ce4c5da80 100644 --- a/src/plugins/vis_types/timeseries/server/usage_collector/register_timeseries_collector.ts +++ b/src/plugins/vis_types/timeseries/server/usage_collector/register_timeseries_collector.ts @@ -18,6 +18,10 @@ export function registerTimeseriesUsageCollector(collectorSet: UsageCollectionSe type: 'long', _meta: { description: 'Number of TSVB visualizations using "last value" as a time range' }, }, + timeseries_table_use_aggregate_function: { + type: 'long', + _meta: { description: 'Number of TSVB table visualizations using aggregate function' }, + }, }, fetch: async ({ soClient }) => await getStats(soClient), }); diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index d8f098fdc1fcf2..2d1d085198bb48 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -20,7 +20,11 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be.above(0); + expect(resp.body.length).to.be.above(2); // Should at least have registered the three sample data-sets + + ['flights', 'logs', 'ecommerce'].forEach((sampleData) => { + expect(resp.body.findIndex((c: { id: string }) => c.id === sampleData)).to.be.above(-1); + }); }); }); } diff --git a/src/plugins/kibana_legacy/public/angular/watch_multi.d.ts b/typings/js_sql_parser.d.ts similarity index 85% rename from src/plugins/kibana_legacy/public/angular/watch_multi.d.ts rename to typings/js_sql_parser.d.ts index 5d2031148f3dec..b58091d0117e31 100644 --- a/src/plugins/kibana_legacy/public/angular/watch_multi.d.ts +++ b/typings/js_sql_parser.d.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export function watchMultiDecorator($provide: unknown): void; +declare module 'js-sql-parser'; diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 43a779407d2a49..eb7c74a4540be3 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -33,6 +33,8 @@ export function getSeverityColor(score: number) { return mlGetSeverityColor(score); } +export const ML_TRANSACTION_LATENCY_DETECTOR_INDEX = 0; + export const ML_ERRORS = { INVALID_LICENSE: i18n.translate( 'xpack.apm.anomaly_detection.error.invalid_license', diff --git a/x-pack/plugins/apm/common/utils/apm_ml_anomaly_query.ts b/x-pack/plugins/apm/common/utils/apm_ml_anomaly_query.ts new file mode 100644 index 00000000000000..26b859d37cf7f4 --- /dev/null +++ b/x-pack/plugins/apm/common/utils/apm_ml_anomaly_query.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function apmMlAnomalyQuery(detectorIndex: 0 | 1 | 2) { + return [ + { + bool: { + filter: [ + { + terms: { + result_type: ['model_plot', 'record'], + }, + }, + { + term: { detector_index: detectorIndex }, + }, + ], + }, + }, + ]; +} diff --git a/x-pack/plugins/apm/dev_docs/feature_flags.md b/x-pack/plugins/apm/dev_docs/feature_flags.md deleted file mode 100644 index 9f722dd5eac5a0..00000000000000 --- a/x-pack/plugins/apm/dev_docs/feature_flags.md +++ /dev/null @@ -1,14 +0,0 @@ -## Feature flags - -To set up a flagged feature, add the name of the feature key (`apm:myFeature`) to [commmon/ui_settings_keys.ts](./common/ui_settings_keys.ts) and the feature parameters to [server/ui_settings.ts](./server/ui_settings.ts). - -Test for the feature like: - -```js -import { myFeatureEnabled } from '../ui_settings_keys'; -if (core.uiSettings.get(myFeatureEnabled)) { - doStuff(); -} -``` - -Settings can be managed in Kibana under Stack Management > Advanced Settings > Observability. \ No newline at end of file diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts new file mode 100644 index 00000000000000..42da37aa7ef57e --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts @@ -0,0 +1,108 @@ +/* + * 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. + */ + +describe('Rules', () => { + describe('Error count', () => { + const ruleName = 'Error count threshold'; + const comboBoxInputSelector = + '.euiPopover__panel-isOpen [data-test-subj=comboBoxSearchInput]'; + const confirmModalButtonSelector = + '.euiModal button[data-test-subj=confirmModalConfirmButton]'; + const deleteButtonSelector = + '[data-test-subj=deleteActionHoverButton]:first'; + const editButtonSelector = '[data-test-subj=editActionHoverButton]:first'; + + describe('when created from APM', () => { + describe('when created from Service Inventory', () => { + before(() => { + cy.loginAsPowerUser(); + }); + + it('creates and updates a rule', () => { + // Create a rule in APM + cy.visit('/app/apm/services'); + cy.contains('Alerts and rules').click(); + cy.contains('Error count').click(); + cy.contains('Create threshold rule').click(); + + // Change the environment to "testing" + cy.contains('Environment All').click(); + cy.get(comboBoxInputSelector).type('testing{enter}'); + + // Save, with no actions + cy.contains('button:not(:disabled)', 'Save').click(); + cy.get(confirmModalButtonSelector).click(); + + cy.contains(`Created rule "${ruleName}`); + + // Go to Stack Management + cy.contains('Alerts and rules').click(); + cy.contains('Manage rules').click(); + + // Edit the rule, changing the environment to "All" + cy.get(editButtonSelector).click(); + cy.contains('Environment testing').click(); + cy.get(comboBoxInputSelector).type('All{enter}'); + cy.contains('button:not(:disabled)', 'Save').click(); + + cy.contains(`Updated '${ruleName}'`); + + // Wait for the table to be ready for next edit click + cy.get('.euiBasicTable').not('.euiBasicTable-loading'); + + // Ensure the rule now shows "All" for the environment + cy.get(editButtonSelector).click(); + cy.contains('Environment All'); + cy.contains('button', 'Cancel').click(); + + // Delete the rule + cy.get(deleteButtonSelector).click(); + cy.get(confirmModalButtonSelector).click(); + + // Ensure the table is empty + cy.contains('Create your first rule'); + }); + }); + }); + + describe('when created from Stack management', () => { + before(() => { + cy.loginAsPowerUser(); + }); + + it('creates a rule', () => { + // Go to stack management + cy.visit('/app/management/insightsAndAlerting/triggersActions/rules'); + + // Create a rule + cy.contains('button', 'Create rule').click(); + cy.get('[name=name]').type(ruleName); + cy.contains('.euiFlyout button', ruleName).click(); + + // Change the environment to "testing" + cy.contains('Environment All').click(); + cy.get(comboBoxInputSelector).type('testing{enter}'); + + // Save, with no actions + cy.contains('button:not(:disabled)', 'Save').click(); + cy.get(confirmModalButtonSelector).click(); + + cy.contains(`Created rule "${ruleName}`); + + // Wait for the table to be ready for next delete click + cy.get('.euiBasicTable').not('.euiBasicTable-loading'); + + // Delete the rule + cy.get(deleteButtonSelector).click(); + cy.get(confirmModalButtonSelector).click(); + + // Ensure the table is empty + cy.contains('Create your first rule'); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts index 9d4c773422cdc4..679e2934f9c37a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts @@ -17,11 +17,11 @@ const serviceInventoryHref = url.format({ const apisToIntercept = [ { - endpoint: '/api/apm/service', + endpoint: '/api/apm/service?*', name: 'servicesMainStatistics', }, { - endpoint: '/api/apm/services/detailed_statistics', + endpoint: '/api/apm/services/detailed_statistics?*', name: 'servicesDetailedStatistics', }, ]; diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.stories.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.stories.tsx new file mode 100644 index 00000000000000..d28d3076b21c0f --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.stories.tsx @@ -0,0 +1,152 @@ +/* + * 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 { Meta, Story } from '@storybook/react'; +import React, { useState } from 'react'; +import { AlertParams, ErrorCountAlertTrigger } from '.'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { AlertMetadata } from '../helper'; + +const KibanaReactContext = createKibanaReactContext({ + notifications: { toasts: { add: () => {} } }, +} as unknown as Partial); + +interface Args { + alertParams: AlertParams; + metadata?: AlertMetadata; +} + +const stories: Meta<{}> = { + title: 'alerting/ErrorCountAlertTrigger', + component: ErrorCountAlertTrigger, + decorators: [ + (StoryComponent) => { + return ( + +
+ +
+
+ ); + }, + ], +}; +export default stories; + +export const CreatingInApmFromInventory: Story = ({ + alertParams, + metadata, +}) => { + const [params, setParams] = useState(alertParams); + + function setAlertParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; +CreatingInApmFromInventory.args = { + alertParams: {}, + metadata: { + end: '2021-09-10T14:14:04.789Z', + environment: ENVIRONMENT_ALL.value, + serviceName: undefined, + start: '2021-09-10T13:59:00.000Z', + }, +}; + +export const CreatingInApmFromService: Story = ({ + alertParams, + metadata, +}) => { + const [params, setParams] = useState(alertParams); + + function setAlertParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; +CreatingInApmFromService.args = { + alertParams: {}, + metadata: { + end: '2021-09-10T14:14:04.789Z', + environment: 'testEnvironment', + serviceName: 'testServiceName', + start: '2021-09-10T13:59:00.000Z', + }, +}; + +export const EditingInStackManagement: Story = ({ + alertParams, + metadata, +}) => { + const [params, setParams] = useState(alertParams); + + function setAlertParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; +EditingInStackManagement.args = { + alertParams: { + environment: 'testEnvironment', + serviceName: 'testServiceName', + threshold: 25, + windowSize: 1, + windowUnit: 'm', + }, + metadata: undefined, +}; + +export const CreatingInStackManagement: Story = ({ + alertParams, + metadata, +}) => { + const [params, setParams] = useState(alertParams); + + function setAlertParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; +CreatingInStackManagement.args = { + alertParams: {}, + metadata: undefined, +}; diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.test.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.test.tsx new file mode 100644 index 00000000000000..26c62b10e6220e --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.test.tsx @@ -0,0 +1,19 @@ +/* + * 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 { render } from '@testing-library/react'; +import React from 'react'; +import * as stories from './error_count_alert_trigger.stories'; +import { composeStories } from '@storybook/testing-react'; + +const { CreatingInApmFromService } = composeStories(stories); + +describe('ErrorCountAlertTrigger', () => { + it('renders', () => { + expect(() => render()).not.toThrowError(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx deleted file mode 100644 index b6ee1a61cea5af..00000000000000 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { AlertParams, ErrorCountAlertTrigger } from '.'; -import { CoreStart } from '../../../../../../../src/core/public'; -import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; - -const KibanaReactContext = createKibanaReactContext({ - notifications: { toasts: { add: () => {} } }, -} as unknown as Partial); - -export default { - title: 'alerting/ErrorCountAlertTrigger', - component: ErrorCountAlertTrigger, - decorators: [ - (Story: React.ComponentClass) => ( - -
- -
-
- ), - ], -}; - -export function Example() { - const [params, setParams] = useState({ - serviceName: 'testServiceName', - environment: 'testEnvironment', - threshold: 2, - windowSize: 5, - windowUnit: 'm', - }); - - function setAlertParams(property: string, value: any) { - setParams({ ...params, [property]: value }); - } - - return ( - {}} - /> - ); -} diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index dd94cf4b175a69..cb7b367fb390b8 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -7,29 +7,25 @@ import { i18n } from '@kbn/i18n'; import { defaults, omit } from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { asInteger } from '../../../../common/utils/formatters'; -import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher'; +import { createCallApmApi } from '../../../services/rest/createCallApmApi'; import { ChartPreview } from '../chart_preview'; import { EnvironmentField, IsAboveField, ServiceField } from '../fields'; -import { - AlertMetadata, - getIntervalAndTimeRange, - isNewApmRuleFromStackManagement, - TimeUnit, -} from '../helper'; -import { NewAlertEmptyPrompt } from '../new_alert_empty_prompt'; +import { AlertMetadata, getIntervalAndTimeRange, TimeUnit } from '../helper'; import { ServiceAlertTrigger } from '../service_alert_trigger'; export interface AlertParams { - windowSize: number; - windowUnit: string; - threshold: number; - serviceName: string; - environment: string; + windowSize?: number; + windowUnit?: TimeUnit; + threshold?: number; + serviceName?: string; + environment?: string; } interface Props { @@ -40,13 +36,12 @@ interface Props { } export function ErrorCountAlertTrigger(props: Props) { + const { services } = useKibana(); const { alertParams, metadata, setAlertParams, setAlertProperty } = props; - const { environmentOptions } = useEnvironmentsFetcher({ - serviceName: metadata?.serviceName, - start: metadata?.start, - end: metadata?.end, - }); + useEffect(() => { + createCallApmApi(services as CoreStart); + }, [services]); const params = defaults( { ...omit(metadata, ['start', 'end']), ...alertParams }, @@ -87,16 +82,14 @@ export function ErrorCountAlertTrigger(props: Props) { ] ); - if (isNewApmRuleFromStackManagement(alertParams, metadata)) { - return ; - } - const fields = [ - , + setAlertParams('serviceName', value)} + />, setAlertParams('environment', e.target.value)} + onChange={(value) => setAlertParams('environment', value)} />, { describe('Service Field', () => { it('renders with value', () => { - const component = render(); + const component = render( + {}} /> + ); expectTextsInDocument(component, ['foo']); }); it('renders with All when value is not defined', () => { - const component = render(); + const component = render( {}} />); expectTextsInDocument(component, ['All']); }); }); - describe('Transaction Type Field', () => { - it('renders select field when multiple options available', () => { - const options = [ - { text: 'Foo', value: 'foo' }, - { text: 'Bar', value: 'bar' }, - ]; - const { getByText, getByTestId } = render( - - ); - act(() => { - fireEvent.click(getByText('Foo')); - }); - - const selectBar = getByTestId('transactionTypeField'); - expect(selectBar instanceof HTMLSelectElement).toBeTruthy(); - const selectOptions = (selectBar as HTMLSelectElement).options; - expect(selectOptions.length).toEqual(2); - expect( - Object.values(selectOptions).map((option) => option.value) - ).toEqual(['foo', 'bar']); - }); - it('renders read-only field when single option available', () => { - const options = [{ text: 'Bar', value: 'bar' }]; + describe('TransactionTypeField', () => { + it('renders', () => { const component = render( - + {}} /> ); expectTextsInDocument(component, ['Bar']); }); - it('renders read-only All option when no option available', () => { - const component = render(); - expectTextsInDocument(component, ['All']); - }); it('renders current value when available', () => { - const component = render(); + const component = render( + {}} /> + ); expectTextsInDocument(component, ['foo']); }); }); diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index e4826351523650..171953ea522eb4 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -5,59 +5,96 @@ * 2.0. */ -import { EuiSelect, EuiExpression, EuiFieldNumber } from '@elastic/eui'; -import React from 'react'; +import { EuiComboBoxOptionOption, EuiFieldNumber } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiSelectOption } from '@elastic/eui'; +import React from 'react'; +import { + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, getEnvironmentLabel, } from '../../../common/environment_filter_values'; +import { SuggestionsSelect } from '../shared/suggestions_select'; import { PopoverExpression } from './service_alert_trigger/popover_expression'; -const ALL_OPTION = i18n.translate('xpack.apm.alerting.fields.all_option', { +const allOptionText = i18n.translate('xpack.apm.alerting.fields.allOption', { defaultMessage: 'All', }); +const allOption: EuiComboBoxOptionOption = { + label: allOptionText, + value: allOptionText, +}; +const environmentAllOption: EuiComboBoxOptionOption = { + label: ENVIRONMENT_ALL.text, + value: ENVIRONMENT_ALL.value, +}; -export function ServiceField({ value }: { value?: string }) { +export function ServiceField({ + allowAll = true, + currentValue, + onChange, +}: { + allowAll?: boolean; + currentValue?: string; + onChange: (value?: string) => void; +}) { return ( - + > + + ); } export function EnvironmentField({ currentValue, - options, onChange, }: { currentValue: string; - options: EuiSelectOption[]; - onChange: (event: React.ChangeEvent) => void; + onChange: (value?: string) => void; }) { - const title = i18n.translate('xpack.apm.alerting.fields.environment', { - defaultMessage: 'Environment', - }); - if (options.length === 1) { - return ( - - ); - } - return ( - - + ); @@ -65,31 +102,33 @@ export function EnvironmentField({ export function TransactionTypeField({ currentValue, - options, onChange, }: { currentValue?: string; - options?: EuiSelectOption[]; - onChange?: (event: React.ChangeEvent) => void; + onChange: (value?: string) => void; }) { const label = i18n.translate('xpack.apm.alerting.fields.type', { defaultMessage: 'Type', }); - - if (!options || options.length <= 1) { - return ( - - ); - } - return ( - - + ); diff --git a/x-pack/plugins/apm/public/components/alerting/helper.ts b/x-pack/plugins/apm/public/components/alerting/helper.ts index b3dac5c2643dbb..4032c33fa30b70 100644 --- a/x-pack/plugins/apm/public/components/alerting/helper.ts +++ b/x-pack/plugins/apm/public/components/alerting/helper.ts @@ -36,14 +36,3 @@ export function getIntervalAndTimeRange({ end: new Date(end).toISOString(), }; } - -export function isNewApmRuleFromStackManagement( - alertParams: any, - metadata?: AlertMetadata -) { - return ( - alertParams !== undefined && - Object.keys(alertParams).length === 0 && - metadata === undefined - ); -} diff --git a/x-pack/plugins/apm/public/components/alerting/new_alert_empty_prompt.tsx b/x-pack/plugins/apm/public/components/alerting/new_alert_empty_prompt.tsx deleted file mode 100644 index 4777da7871b68c..00000000000000 --- a/x-pack/plugins/apm/public/components/alerting/new_alert_empty_prompt.tsx +++ /dev/null @@ -1,47 +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. - */ - -/* eslint-disable @elastic/eui/href-or-on-click */ - -import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { MouseEvent } from 'react'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; - -export function NewAlertEmptyPrompt() { - const { services } = useKibana(); - const apmUrl = services.http?.basePath.prepend('/app/apm'); - const navigateToUrl = services.application?.navigateToUrl; - const handleClick = (event: MouseEvent) => { - event.preventDefault(); - if (apmUrl && navigateToUrl) { - navigateToUrl(apmUrl); - } - }; - - return ( - - {i18n.translate('xpack.apm.NewAlertEmptyPrompt.goToApmLinkText', { - defaultMessage: 'Go to APM', - })} - , - ]} - /> - ); -} diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index dbbb7186de65c8..5327eb561bfc61 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -8,14 +8,12 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { defaults, map, omit } from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; import { CoreStart } from '../../../../../../../src/core/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getDurationFormatter } from '../../../../common/utils/formatters'; -import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher'; -import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher'; import { createCallApmApi } from '../../../services/rest/createCallApmApi'; import { @@ -29,13 +27,7 @@ import { ServiceField, TransactionTypeField, } from '../fields'; -import { - AlertMetadata, - getIntervalAndTimeRange, - isNewApmRuleFromStackManagement, - TimeUnit, -} from '../helper'; -import { NewAlertEmptyPrompt } from '../new_alert_empty_prompt'; +import { AlertMetadata, getIntervalAndTimeRange, TimeUnit } from '../helper'; import { ServiceAlertTrigger } from '../service_alert_trigger'; import { PopoverExpression } from '../service_alert_trigger/popover_expression'; @@ -81,13 +73,9 @@ export function TransactionDurationAlertTrigger(props: Props) { const { services } = useKibana(); const { alertParams, metadata, setAlertParams, setAlertProperty } = props; - createCallApmApi(services as CoreStart); - - const transactionTypes = useServiceTransactionTypesFetcher({ - serviceName: metadata?.serviceName, - start: metadata?.start, - end: metadata?.end, - }); + useEffect(() => { + createCallApmApi(services as CoreStart); + }, [services]); const params = defaults( { @@ -100,16 +88,9 @@ export function TransactionDurationAlertTrigger(props: Props) { windowSize: 5, windowUnit: 'm', environment: ENVIRONMENT_ALL.value, - transactionType: transactionTypes[0], } ); - const { environmentOptions } = useEnvironmentsFetcher({ - serviceName: params.serviceName, - start: metadata?.start, - end: metadata?.end, - }); - const { data } = useFetcher( (callApmApi) => { const { interval, start, end } = getIntervalAndTimeRange({ @@ -160,25 +141,19 @@ export function TransactionDurationAlertTrigger(props: Props) { /> ); - if (isNewApmRuleFromStackManagement(alertParams, metadata)) { - return ; - } - - if (!params.serviceName) { - return null; - } - const fields = [ - , + setAlertParams('serviceName', value)} + />, ({ text: key, value: key }))} - onChange={(e) => setAlertParams('transactionType', e.target.value)} + onChange={(value) => setAlertParams('transactionType', value)} />, setAlertParams('environment', e.target.value)} + onChange={(value) => setAlertParams('environment', value)} />, { + createCallApmApi(services as CoreStart); + }, [services]); const params = defaults( { @@ -68,27 +67,18 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { } ); - const { environmentOptions } = useEnvironmentsFetcher({ - serviceName: params.serviceName, - start: metadata?.start, - end: metadata?.end, - }); - - if (isNewApmRuleFromStackManagement(alertParams, metadata)) { - return ; - } - const fields = [ - , + setAlertParams('serviceName', value)} + />, ({ text: key, value: key }))} - onChange={(e) => setAlertParams('transactionType', e.target.value)} + onChange={(value) => setAlertParams('transactionType', value)} />, setAlertParams('environment', e.target.value)} + onChange={(value) => setAlertParams('environment', value)} />, } diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index 22e8bef9bc7800..3bad7ae15f658c 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -6,14 +6,12 @@ */ import { defaults, omit } from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; import { CoreStart } from '../../../../../../../src/core/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { asPercent } from '../../../../common/utils/formatters'; -import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher'; -import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher'; import { createCallApmApi } from '../../../services/rest/createCallApmApi'; import { ChartPreview } from '../chart_preview'; @@ -23,22 +21,16 @@ import { ServiceField, TransactionTypeField, } from '../fields'; -import { - AlertMetadata, - getIntervalAndTimeRange, - isNewApmRuleFromStackManagement, - TimeUnit, -} from '../helper'; -import { NewAlertEmptyPrompt } from '../new_alert_empty_prompt'; +import { AlertMetadata, getIntervalAndTimeRange, TimeUnit } from '../helper'; import { ServiceAlertTrigger } from '../service_alert_trigger'; interface AlertParams { - windowSize: number; - windowUnit: string; - threshold: number; - serviceName: string; - transactionType: string; - environment: string; + windowSize?: number; + windowUnit?: string; + threshold?: number; + serviceName?: string; + transactionType?: string; + environment?: string; } interface Props { @@ -52,12 +44,9 @@ export function TransactionErrorRateAlertTrigger(props: Props) { const { services } = useKibana(); const { alertParams, metadata, setAlertParams, setAlertProperty } = props; - createCallApmApi(services as CoreStart); - const transactionTypes = useServiceTransactionTypesFetcher({ - serviceName: metadata?.serviceName, - start: metadata?.start, - end: metadata?.end, - }); + useEffect(() => { + createCallApmApi(services as CoreStart); + }, [services]); const params = defaults( { ...omit(metadata, ['start', 'end']), ...alertParams }, @@ -69,12 +58,6 @@ export function TransactionErrorRateAlertTrigger(props: Props) { } ); - const { environmentOptions } = useEnvironmentsFetcher({ - serviceName: params.serviceName, - start: metadata?.start, - end: metadata?.end, - }); - const thresholdAsPercent = (params.threshold ?? 0) / 100; const { data } = useFetcher( @@ -108,21 +91,18 @@ export function TransactionErrorRateAlertTrigger(props: Props) { ] ); - if (isNewApmRuleFromStackManagement(alertParams, metadata)) { - return ; - } - const fields = [ - , + setAlertParams('serviceName', value)} + />, ({ text: key, value: key }))} - onChange={(e) => setAlertParams('transactionType', e.target.value)} + onChange={(value) => setAlertParams('transactionType', value)} />, setAlertParams('environment', e.target.value)} + onChange={(value) => setAlertParams('environment', value)} />, ; + customOptionText: string; + defaultValue?: string; + field: string; + onChange: (value?: string) => void; + placeholder: string; +} + +export function SuggestionsSelect({ + allOption, + customOptionText, + defaultValue, + field, + onChange, + placeholder, +}: SuggestionsSelectProps) { + const allowAll = !!allOption; + let defaultOption: EuiComboBoxOptionOption | undefined; + + if (allowAll && !defaultValue) { + defaultOption = allOption; + } + if (defaultValue) { + defaultOption = { label: defaultValue, value: defaultValue }; + } + const [selectedOptions, setSelectedOptions] = useState( + defaultOption ? [defaultOption] : [] + ); + + const [searchValue, setSearchValue] = useState(''); + + const { data, status } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /internal/apm/suggestions', + params: { + query: { field, string: searchValue }, + }, + }); + }, + [field, searchValue], + { preservePreviousData: false } + ); + + const handleChange = useCallback( + (changedOptions: Array>) => { + setSelectedOptions(changedOptions); + if (changedOptions.length === 1) { + onChange( + changedOptions[0].value + ? changedOptions[0].value.trim() + : changedOptions[0].value + ); + } + }, + [onChange] + ); + + const handleCreateOption = useCallback( + (value: string) => { + handleChange([{ label: value, value }]); + }, + [handleChange] + ); + + const terms = data?.terms ?? []; + + const options: Array> = [ + ...(allOption && + (searchValue === '' || + searchValue.toLowerCase() === allOption.label.toLowerCase()) + ? [allOption] + : []), + ...terms.map((name) => { + return { label: name, value: name }; + }), + ]; + + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.stories.tsx b/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.stories.tsx new file mode 100644 index 00000000000000..d83ca13a9bb218 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.stories.tsx @@ -0,0 +1,77 @@ +/* + * 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 { EuiComboBoxOptionOption } from '@elastic/eui'; +import { Meta, Story } from '@storybook/react'; +import React from 'react'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import { createCallApmApi } from '../../../services/rest/createCallApmApi'; +import { SuggestionsSelect } from './'; + +interface Args { + allOption: EuiComboBoxOptionOption; + customOptionText: string; + field: string; + placeholder: string; + terms: string[]; +} + +const stories: Meta = { + title: 'shared/SuggestionsSelect', + component: SuggestionsSelect, + decorators: [ + (StoryComponent, { args }) => { + const { terms } = args; + + const coreMock = { + http: { + get: () => { + return { terms }; + }, + }, + notifications: { toasts: { add: () => {} } }, + uiSettings: { get: () => true }, + } as unknown as CoreStart; + + const KibanaReactContext = createKibanaReactContext(coreMock); + + createCallApmApi(coreMock); + + return ( + + + + ); + }, + ], +}; +export default stories; + +export const Example: Story = ({ + allOption, + customOptionText, + field, + placeholder, +}) => { + return ( + {}} + placeholder={placeholder} + /> + ); +}; +Example.args = { + allOption: { label: 'All the things', value: 'ALL_THE_THINGS' }, + terms: ['thing1', 'thing2'], + customOptionText: 'Add {searchValue} as a new thing', + field: 'test.field', + placeholder: 'Select thing', +}; diff --git a/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.test.tsx b/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.test.tsx new file mode 100644 index 00000000000000..b1fce1c439f325 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.test.tsx @@ -0,0 +1,19 @@ +/* + * 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 { composeStories } from '@storybook/testing-react'; +import { render } from '@testing-library/react'; +import React from 'react'; +import * as stories from './suggestions_select.stories'; + +const { Example } = composeStories(stories); + +describe('SuggestionsSelect', () => { + it('renders', () => { + expect(() => render()).not.toThrowError(); + }); +}); diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index fe7e77d28986cf..b78fd6162e7365 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -27,3 +27,4 @@ All files with a .stories.tsx extension will be loaded. You can access the devel - [Routing and Linking](./dev_docs/routing_and_linking.md) - [Telemetry](./dev_docs/telemetry.md) - [Features flags](./dev_docs/feature_flags.md) +- [Official APM UI settings docs](https://www.elastic.co/guide/en/kibana/current/apm-settings-in-kibana.html) diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 2c21ff17f779ba..b7002ff7cbe797 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -7,12 +7,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { - PluginInitializerContext, PluginConfigDescriptor, + PluginInitializerContext, } from 'src/core/server'; import { APMOSSConfig } from 'src/plugins/apm_oss/server'; -import { APMPlugin } from './plugin'; +import { maxSuggestions } from '../../observability/common'; import { SearchAggregatedTransactionSetting } from '../common/aggregated_transactions'; +import { APMPlugin } from './plugin'; const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -40,8 +41,6 @@ const configSchema = schema.object({ ), telemetryCollectionEnabled: schema.boolean({ defaultValue: true }), metricsInterval: schema.number({ defaultValue: 30 }), - maxServiceEnvironments: schema.number({ defaultValue: 100 }), - maxServiceSelection: schema.number({ defaultValue: 50 }), profilingEnabled: schema.boolean({ defaultValue: false }), agent: schema.object({ migrations: schema.object({ @@ -52,7 +51,17 @@ const configSchema = schema.object({ // plugin config export const config: PluginConfigDescriptor = { - deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], + deprecations: ({ deprecate, renameFromRoot }) => [ + deprecate('enabled', '8.0.0'), + renameFromRoot( + 'xpack.apm.maxServiceEnvironments', + `uiSettings.overrides[${maxSuggestions}]` + ), + renameFromRoot( + 'xpack.apm.maxServiceSelections', + `uiSettings.overrides[${maxSuggestions}]` + ), + ], exposeToBrowser: { serviceMapEnabled: true, ui: true, @@ -91,8 +100,6 @@ export function mergeConfigs( 'xpack.apm.serviceMapMaxTracesPerRequest': apmConfig.serviceMapMaxTracesPerRequest, 'xpack.apm.ui.enabled': apmConfig.ui.enabled, - 'xpack.apm.maxServiceEnvironments': apmConfig.maxServiceEnvironments, - 'xpack.apm.maxServiceSelection': apmConfig.maxServiceSelection, 'xpack.apm.ui.maxTraceItems': apmConfig.ui.maxTraceItems, 'xpack.apm.ui.transactionGroupBucketSize': apmConfig.ui.transactionGroupBucketSize, diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 1ea9ae6b65ac53..324202b207237e 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -5,21 +5,21 @@ * 2.0. */ +import Boom from '@hapi/boom'; import { Logger } from 'kibana/server'; -import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; -import Boom from '@hapi/boom'; import moment from 'moment'; +import uuid from 'uuid/v4'; import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { ProcessorEvent } from '../../../common/processor_event'; -import { environmentQuery } from '../../../common/utils/environment_query'; -import { Setup } from '../helpers/setup_request'; import { - TRANSACTION_DURATION, + METRICSET_NAME, PROCESSOR_EVENT, } from '../../../common/elasticsearch_fieldnames'; -import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { environmentQuery } from '../../../common/utils/environment_query'; import { withApmSpan } from '../../utils/with_apm_span'; +import { Setup } from '../helpers/setup_request'; +import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; import { getAnomalyDetectionJobs } from './get_anomaly_detection_jobs'; export async function createAnomalyDetectionJobs( @@ -50,7 +50,7 @@ export async function createAnomalyDetectionJobs( `Creating ML anomaly detection jobs for environments: [${uniqueMlJobEnvs}].` ); - const indexPatternName = indices['apm_oss.transactionIndices']; + const indexPatternName = indices['apm_oss.metricsIndices']; const responses = await Promise.all( uniqueMlJobEnvs.map((environment) => createAnomalyDetectionJob({ ml, environment, indexPatternName }) @@ -92,8 +92,8 @@ async function createAnomalyDetectionJob({ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { exists: { field: TRANSACTION_DURATION } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, + { term: { [METRICSET_NAME]: 'transaction' } }, ...environmentQuery(environment), ], }, @@ -105,7 +105,7 @@ async function createAnomalyDetectionJob({ job_tags: { environment, // identifies this as an APM ML job & facilitates future migrations - apm_ml_version: 2, + apm_ml_version: 3, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap index da2309afa07cf7..61f5b575a5200c 100644 --- a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap @@ -15,7 +15,7 @@ Object { "terms": Object { "field": "service.environment", "missing": undefined, - "size": 100, + "size": 50, }, }, }, @@ -50,7 +50,7 @@ Object { "terms": Object { "field": "service.environment", "missing": "ENVIRONMENT_NOT_DEFINED", - "size": 100, + "size": 50, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap index 47c5e9033eb0ca..d35aeda9e8681b 100644 --- a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap @@ -15,7 +15,7 @@ Object { "terms": Object { "field": "service.environment", "missing": "ENVIRONMENT_NOT_DEFINED", - "size": 100, + "size": 50, }, }, }, @@ -59,7 +59,7 @@ Object { "terms": Object { "field": "service.environment", "missing": "ENVIRONMENT_NOT_DEFINED", - "size": 100, + "size": 50, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts index 2c1772cc5800a8..0f27839d940486 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts @@ -21,9 +21,10 @@ describe('getAllEnvironments', () => { it('fetches all environments', async () => { mock = await inspectSearchParams((setup) => getAllEnvironments({ - serviceName: 'test', searchAggregatedTransactions: false, + serviceName: 'test', setup, + size: 50, }) ); @@ -33,10 +34,11 @@ describe('getAllEnvironments', () => { it('fetches all environments with includeMissing', async () => { mock = await inspectSearchParams((setup) => getAllEnvironments({ + includeMissing: true, + searchAggregatedTransactions: false, serviceName: 'test', setup, - searchAggregatedTransactions: false, - includeMissing: true, + size: 50, }) ); diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index f6a19879748532..1ddc3f7ed888c9 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -19,22 +19,23 @@ import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregate * It's used in places where we get the list of all possible environments. */ export async function getAllEnvironments({ + includeMissing = false, + searchAggregatedTransactions, serviceName, setup, - searchAggregatedTransactions, - includeMissing = false, + size, }: { + includeMissing?: boolean; + searchAggregatedTransactions: boolean; serviceName?: string; setup: Setup; - searchAggregatedTransactions: boolean; - includeMissing?: boolean; + size: number; }) { const operationName = serviceName ? 'get_all_environments_for_service' : 'get_all_environments_for_all_services'; - const { apmEventClient, config } = setup; - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; + const { apmEventClient } = setup; // omit filter for service.name if "All" option is selected const serviceNameFilter = serviceName @@ -65,7 +66,7 @@ export async function getAllEnvironments({ environments: { terms: { field: SERVICE_ENVIRONMENT, - size: maxServiceEnvironments, + size, ...(!serviceName ? { min_doc_count: 0 } : {}), missing: includeMissing ? ENVIRONMENT_NOT_DEFINED.value : undefined, }, diff --git a/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts index 26c4ee85e7d8b9..472fd9d226e354 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts @@ -24,6 +24,7 @@ describe('getEnvironments', () => { setup, serviceName: 'foo', searchAggregatedTransactions: false, + size: 50, start: 0, end: 50000, }) @@ -37,6 +38,7 @@ describe('getEnvironments', () => { getEnvironments({ setup, searchAggregatedTransactions: false, + size: 50, start: 0, end: 50000, }) diff --git a/x-pack/plugins/apm/server/lib/environments/get_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.ts index d87cdbe85e73fa..08f6f089e8d083 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.ts @@ -20,15 +20,17 @@ import { Setup } from '../helpers/setup_request'; * filtered by range. */ export async function getEnvironments({ - setup, - serviceName, searchAggregatedTransactions, + serviceName, + setup, + size, start, end, }: { setup: Setup; serviceName?: string; searchAggregatedTransactions: boolean; + size: number; start: number; end: number; }) { @@ -36,7 +38,7 @@ export async function getEnvironments({ ? 'get_environments_for_service' : 'get_environments'; - const { apmEventClient, config } = setup; + const { apmEventClient } = setup; const filter = rangeQuery(start, end); @@ -46,8 +48,6 @@ export async function getEnvironments({ }); } - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; - const params = { apm: { events: [ @@ -70,7 +70,7 @@ export async function getEnvironments({ terms: { field: SERVICE_ENVIRONMENT, missing: ENVIRONMENT_NOT_DEFINED.value, - size: maxServiceEnvironments, + size, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 3fed3c92c440c6..b2b2a0b869c801 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -5,6 +5,10 @@ * 2.0. */ +import type { + TermsEnumRequest, + TermsEnumResponse, +} from '@elastic/elasticsearch/api/types'; import { ValuesType } from 'utility-types'; import { withApmSpan } from '../../../../utils/with_apm_span'; import { Profile } from '../../../../../typings/es_schemas/ui/profile'; @@ -39,6 +43,10 @@ export type APMEventESSearchRequest = Omit & { }; }; +export type APMEventESTermsEnumRequest = Omit & { + apm: { events: ProcessorEvent[] }; +}; + // These keys shoul all be `ProcessorEvent.x`, but until TypeScript 4.2 we're inlining them here. // See https://github.com/microsoft/TypeScript/issues/37888 type TypeOfProcessorEvent = { @@ -124,5 +132,44 @@ export function createApmEventClient({ requestParams: searchParams, }); }, + + async termsEnum( + operationName: string, + params: APMEventESTermsEnumRequest + ): Promise { + const requestType = 'terms_enum'; + const { index } = unpackProcessorEvents(params, indices); + + return callAsyncWithDebug({ + cb: () => { + const { apm, ...rest } = params; + const termsEnumPromise = withApmSpan(operationName, () => + cancelEsRequestOnAbort( + esClient.termsEnum({ + index: Array.isArray(index) ? index.join(',') : index, + ...rest, + }), + request + ) + ); + + return unwrapEsResponse(termsEnumPromise); + }, + getDebugMessage: () => ({ + body: getDebugBody({ + params, + requestType, + operationName, + }), + title: getDebugTitle(request), + }), + isCalledWithInternalUser: false, + debug, + request, + requestType, + operationName, + requestParams: params, + }); + }, }; } diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts index 8732ba81f9ae64..47a2b3fe7e5c8a 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -12,7 +12,7 @@ import { ESSearchRequest, ESFilter, } from '../../../../../../../../src/core/types/elasticsearch'; -import { APMEventESSearchRequest } from '.'; +import { APMEventESSearchRequest, APMEventESTermsEnumRequest } from '.'; import { ApmIndicesConfig, ApmIndicesName, @@ -28,7 +28,7 @@ const processorEventIndexMap: Record = { }; export function unpackProcessorEvents( - request: APMEventESSearchRequest, + request: APMEventESSearchRequest | APMEventESTermsEnumRequest, indices: ApmIndicesConfig ) { const { apm, ...params } = request; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 9b2d79dc726eea..97c95e4e40045c 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -11,7 +11,11 @@ import { estypes } from '@elastic/elasticsearch'; import { ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; import { MlPluginSetup } from '../../../../ml/server'; import { PromiseReturnType } from '../../../../observability/typings/common'; -import { getSeverity, ML_ERRORS } from '../../../common/anomaly_detection'; +import { + getSeverity, + ML_ERRORS, + ML_TRANSACTION_LATENCY_DETECTOR_INDEX, +} from '../../../common/anomaly_detection'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { getServiceHealthStatus } from '../../../common/service_health_status'; import { @@ -22,6 +26,7 @@ import { rangeQuery } from '../../../../observability/server'; import { withApmSpan } from '../../utils/with_apm_span'; import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; import { Setup } from '../helpers/setup_request'; +import { apmMlAnomalyQuery } from '../../../common/utils/apm_ml_anomaly_query'; export const DEFAULT_ANOMALIES: ServiceAnomaliesResponse = { mlJobIds: [], @@ -56,7 +61,7 @@ export async function getServiceAnomalies({ query: { bool: { filter: [ - { terms: { result_type: ['model_plot', 'record'] } }, + ...apmMlAnomalyQuery(ML_TRANSACTION_LATENCY_DETECTOR_INDEX), ...rangeQuery( Math.min(end - 30 * 60 * 1000, start), end, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 18ef3f44331d98..b6b4f2208d04f5 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -99,7 +99,7 @@ Object { "terms": Object { "field": "service.environment", "missing": undefined, - "size": 100, + "size": 50, }, }, }, @@ -127,7 +127,7 @@ Object { "terms": Object { "field": "service.environment", "missing": "ALL_OPTION_VALUE", - "size": 100, + "size": 50, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts index 124a373d3cf070..4fd351f8708a2f 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts @@ -15,12 +15,13 @@ import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration/all_ export async function getExistingEnvironmentsForService({ serviceName, setup, + size, }: { serviceName: string | undefined; setup: Setup; + size: number; }) { - const { internalClient, indices, config } = setup; - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; + const { internalClient, indices } = setup; const bool = serviceName ? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] } @@ -36,7 +37,7 @@ export async function getExistingEnvironmentsForService({ terms: { field: SERVICE_ENVIRONMENT, missing: ALL_OPTION_VALUE, - size: maxServiceEnvironments, + size, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts index 0ab56ac372706e..dadb29d156e0bb 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts @@ -20,15 +20,22 @@ export async function getEnvironments({ serviceName, setup, searchAggregatedTransactions, + size, }: { serviceName: string | undefined; setup: Setup; searchAggregatedTransactions: boolean; + size: number; }) { return withApmSpan('get_environments_for_agent_configuration', async () => { const [allEnvironments, existingEnvironments] = await Promise.all([ - getAllEnvironments({ serviceName, setup, searchAggregatedTransactions }), - getExistingEnvironmentsForService({ serviceName, setup }), + getAllEnvironments({ + searchAggregatedTransactions, + serviceName, + setup, + size, + }), + getExistingEnvironmentsForService({ serviceName, setup, size }), ]); return [ALL_OPTION_VALUE, ...allEnvironments].map((environment) => { diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 0786bc6bc27714..282eacbec66d1b 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -15,15 +15,17 @@ import { getProcessorEventForAggregatedTransactions } from '../../helpers/aggreg export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames >; + export async function getServiceNames({ setup, searchAggregatedTransactions, + size, }: { setup: Setup; searchAggregatedTransactions: boolean; + size: number; }) { - const { apmEventClient, config } = setup; - const maxServiceSelection = config['xpack.apm.maxServiceSelection']; + const { apmEventClient } = setup; const params = { apm: { @@ -42,8 +44,8 @@ export async function getServiceNames({ services: { terms: { field: SERVICE_NAME, - size: maxServiceSelection, min_doc_count: 0, + size, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts index 17f51e8826d9d5..4ffc8ed98184bb 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts @@ -27,9 +27,10 @@ describe('agent configuration queries', () => { it('fetches all environments', async () => { mock = await inspectSearchParams((setup) => getAllEnvironments({ + searchAggregatedTransactions: false, serviceName: 'foo', setup, - searchAggregatedTransactions: false, + size: 50, }) ); @@ -43,6 +44,7 @@ describe('agent configuration queries', () => { getExistingEnvironmentsForService({ serviceName: 'foo', setup, + size: 50, }) ); @@ -56,6 +58,7 @@ describe('agent configuration queries', () => { getServiceNames({ setup, searchAggregatedTransactions: false, + size: 50, }) ); diff --git a/x-pack/plugins/apm/server/lib/suggestions/get_suggestions.ts b/x-pack/plugins/apm/server/lib/suggestions/get_suggestions.ts new file mode 100644 index 00000000000000..acd44366ef4c36 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/suggestions/get_suggestions.ts @@ -0,0 +1,46 @@ +/* + * 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 { ProcessorEvent } from '../../../common/processor_event'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; +import { Setup } from '../helpers/setup_request'; + +export async function getSuggestions({ + field, + searchAggregatedTransactions, + setup, + size, + string, +}: { + field: string; + searchAggregatedTransactions: boolean; + setup: Setup; + size: number; + string: string; +}) { + const { apmEventClient } = setup; + + const response = await apmEventClient.termsEnum('get_suggestions', { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + body: { + case_insensitive: true, + field, + size, + string, + }, + }); + + return { terms: response.terms }; +} diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts index a61e0614f5b1ad..a7357bbc1dd348 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts @@ -12,6 +12,8 @@ import { rangeQuery } from '../../../../../observability/server'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup } from '../../helpers/setup_request'; +import { apmMlAnomalyQuery } from '../../../../common/utils/apm_ml_anomaly_query'; +import { ML_TRANSACTION_LATENCY_DETECTOR_INDEX } from '../../../../common/anomaly_detection'; export type ESResponse = Exclude< PromiseReturnType, @@ -40,7 +42,7 @@ export function anomalySeriesFetcher({ query: { bool: { filter: [ - { terms: { result_type: ['model_plot', 'record'] } }, + ...apmMlAnomalyQuery(ML_TRANSACTION_LATENCY_DETECTOR_INDEX), { term: { partition_field_value: serviceName } }, { term: { by_field_value: transactionType } }, ...rangeQuery(start, end, 'timestamp'), diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts index 95ebb2bf134316..4b00320009e27b 100644 --- a/x-pack/plugins/apm/server/routes/environments.ts +++ b/x-pack/plugins/apm/server/routes/environments.ts @@ -6,6 +6,7 @@ */ import * as t from 'io-ts'; +import { maxSuggestions } from '../../../observability/common'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { setupRequest } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/environments/get_environments'; @@ -26,7 +27,7 @@ const environmentsRoute = createApmServerRoute({ options: { tags: ['access:apm'] }, handler: async (resources) => { const setup = await setupRequest(resources); - const { params } = resources; + const { context, params } = resources; const { serviceName, start, end } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions({ apmEventClient: setup.apmEventClient, @@ -35,11 +36,14 @@ const environmentsRoute = createApmServerRoute({ end, kuery: '', }); - + const size = await context.core.uiSettings.client.get( + maxSuggestions + ); const environments = await getEnvironments({ setup, serviceName, searchAggregatedTransactions, + size, start, end, }); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index 09756e30d96825..7aa520dd5b8a26 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -33,6 +33,7 @@ import { traceRouteRepository } from './traces'; import { transactionRouteRepository } from './transactions'; import { APMRouteHandlerResources } from './typings'; import { historicalDataRouteRepository } from './historical_data'; +import { suggestionsRouteRepository } from './suggestions'; const getTypedGlobalApmServerRouteRepository = () => { const repository = createApmServerRouteRepository() @@ -45,6 +46,7 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(serviceMapRouteRepository) .merge(serviceNodeRouteRepository) .merge(serviceRouteRepository) + .merge(suggestionsRouteRepository) .merge(traceRouteRepository) .merge(transactionRouteRepository) .merge(alertsChartPreviewRouteRepository) diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 589ae41bfdc6a8..a904e5e03b5310 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -8,6 +8,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; import { toBooleanRt } from '@kbn/io-ts-utils'; +import { maxSuggestions } from '../../../../observability/common'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getServiceNames } from '../../lib/settings/agent_configuration/get_service_names'; import { createOrUpdateConfiguration } from '../../lib/settings/agent_configuration/create_or_update_configuration'; @@ -251,9 +252,13 @@ const listAgentConfigurationServicesRoute = createApmServerRoute({ start, end, }); + const size = await resources.context.core.uiSettings.client.get( + maxSuggestions + ); const serviceNames = await getServiceNames({ - setup, searchAggregatedTransactions, + setup, + size, }); return { serviceNames }; @@ -269,7 +274,7 @@ const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({ options: { tags: ['access:apm'] }, handler: async (resources) => { const setup = await setupRequest(resources); - const { params } = resources; + const { context, params } = resources; const { serviceName, start, end } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions({ @@ -279,11 +284,14 @@ const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({ start, end, }); - + const size = await context.core.uiSettings.client.get( + maxSuggestions + ); const environments = await getEnvironments({ serviceName, setup, searchAggregatedTransactions, + size, }); return { environments }; diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index d15f2324118106..c7e45eb8c32e57 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -7,6 +7,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; +import { maxSuggestions } from '../../../../observability/common'; import { isActivePlatinumLicense } from '../../../common/license_check'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { createApmServerRoute } from '../create_apm_server_route'; @@ -92,11 +93,14 @@ const anomalyDetectionEnvironmentsRoute = createApmServerRoute({ config: setup.config, kuery: '', }); - + const size = await resources.context.core.uiSettings.client.get( + maxSuggestions + ); const environments = await getAllEnvironments({ - setup, - searchAggregatedTransactions, includeMissing: true, + searchAggregatedTransactions, + setup, + size, }); return { environments }; diff --git a/x-pack/plugins/apm/server/routes/suggestions.ts b/x-pack/plugins/apm/server/routes/suggestions.ts new file mode 100644 index 00000000000000..8b82601650a481 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/suggestions.ts @@ -0,0 +1,47 @@ +/* + * 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 * as t from 'io-ts'; +import { maxSuggestions } from '../../../observability/common'; +import { getSuggestions } from '../lib/suggestions/get_suggestions'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; + +const suggestionsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/suggestions', + params: t.partial({ + query: t.type({ field: t.string, string: t.string }), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { context, params } = resources; + const { field, string } = params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + kuery: '', + }); + const size = await context.core.uiSettings.client.get( + maxSuggestions + ); + const suggestions = await getSuggestions({ + field, + searchAggregatedTransactions, + setup, + size, + string, + }); + + return suggestions; + }, +}); + +export const suggestionsRouteRepository = + createApmServerRouteRepository().add(suggestionsRoute); diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx index 171af609c25622..7b6b549e07c8d1 100644 --- a/x-pack/plugins/apm/server/utils/test_helpers.tsx +++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx @@ -79,12 +79,6 @@ export async function inspectSearchParams( case 'xpack.apm.metricsInterval': return 30; - - case 'xpack.apm.maxServiceEnvironments': - return 100; - - case 'xpack.apm.maxServiceSelection': - return 50; } }, } diff --git a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot index 8359b186e4afd1..d3ab369dcc32cb 100644 --- a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot @@ -13,7 +13,7 @@ exports[`Storyshots Home Home Page 1`] = ` className="euiPageBody euiPageBody--borderRadiusNone" >
); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts index a9e30e64a003be..396cc402c0ab57 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts @@ -123,6 +123,7 @@ describe('task', () => { const manifestManager = buildManifestManagerMock(); manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(null); + manifestManager.cleanup = jest.fn().mockResolvedValue(null); await runTask(manifestManager); @@ -132,6 +133,7 @@ describe('task', () => { expect(manifestManager.commit).not.toHaveBeenCalled(); expect(manifestManager.tryDispatch).not.toHaveBeenCalled(); expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); + expect(manifestManager.cleanup).not.toHaveBeenCalled(); }); test('Should stop the process when no building new manifest throws error', async () => { @@ -140,6 +142,7 @@ describe('task', () => { manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(lastManifest); manifestManager.buildNewManifest = jest.fn().mockRejectedValue(new Error()); + manifestManager.cleanup = jest.fn().mockResolvedValue(null); await runTask(manifestManager); @@ -149,6 +152,7 @@ describe('task', () => { expect(manifestManager.commit).not.toHaveBeenCalled(); expect(manifestManager.tryDispatch).not.toHaveBeenCalled(); expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); + expect(manifestManager.cleanup).not.toHaveBeenCalled(); }); test('Should recover if last Computed Manifest threw an InvalidInternalManifestError error', async () => { @@ -186,6 +190,7 @@ describe('task', () => { manifestManager.pushArtifacts = jest.fn().mockResolvedValue([]); manifestManager.tryDispatch = jest.fn().mockResolvedValue([]); manifestManager.deleteArtifacts = jest.fn().mockResolvedValue([]); + manifestManager.cleanup = jest.fn().mockResolvedValue(null); await runTask(manifestManager); @@ -197,6 +202,7 @@ describe('task', () => { expect(manifestManager.commit).not.toHaveBeenCalled(); expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); expect(manifestManager.deleteArtifacts).toHaveBeenCalledWith([]); + expect(manifestManager.cleanup).toHaveBeenCalledWith(newManifest); }); test('Should stop the process when there are errors pushing new artifacts', async () => { @@ -211,6 +217,7 @@ describe('task', () => { manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(lastManifest); manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); manifestManager.pushArtifacts = jest.fn().mockResolvedValue([new Error()]); + manifestManager.cleanup = jest.fn().mockResolvedValue(null); await runTask(manifestManager); @@ -225,6 +232,7 @@ describe('task', () => { expect(manifestManager.commit).not.toHaveBeenCalled(); expect(manifestManager.tryDispatch).not.toHaveBeenCalled(); expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); + expect(manifestManager.cleanup).not.toHaveBeenCalled(); }); test('Should stop the process when there are errors committing manifest', async () => { @@ -240,6 +248,7 @@ describe('task', () => { manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); manifestManager.pushArtifacts = jest.fn().mockResolvedValue([]); manifestManager.commit = jest.fn().mockRejectedValue(new Error()); + manifestManager.cleanup = jest.fn().mockResolvedValue(null); await runTask(manifestManager); @@ -254,6 +263,7 @@ describe('task', () => { expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); expect(manifestManager.tryDispatch).not.toHaveBeenCalled(); expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); + expect(manifestManager.cleanup).not.toHaveBeenCalled(); }); test('Should stop the process when there are errors dispatching manifest', async () => { @@ -270,6 +280,7 @@ describe('task', () => { manifestManager.pushArtifacts = jest.fn().mockResolvedValue([]); manifestManager.commit = jest.fn().mockResolvedValue(null); manifestManager.tryDispatch = jest.fn().mockResolvedValue([new Error()]); + manifestManager.cleanup = jest.fn().mockResolvedValue(null); await runTask(manifestManager); @@ -284,6 +295,7 @@ describe('task', () => { expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); + expect(manifestManager.cleanup).not.toHaveBeenCalled(); }); test('Should succeed the process and delete old artifacts', async () => { @@ -303,6 +315,7 @@ describe('task', () => { manifestManager.commit = jest.fn().mockResolvedValue(null); manifestManager.tryDispatch = jest.fn().mockResolvedValue([]); manifestManager.deleteArtifacts = jest.fn().mockResolvedValue([]); + manifestManager.cleanup = jest.fn().mockResolvedValue(null); await runTask(manifestManager); @@ -317,6 +330,7 @@ describe('task', () => { expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); expect(manifestManager.deleteArtifacts).toHaveBeenCalledWith([ARTIFACT_ID_1]); + expect(manifestManager.cleanup).toHaveBeenCalledWith(newManifest); }); test('Should succeed the process but not add or delete artifacts when there are only transitions', async () => { @@ -336,6 +350,7 @@ describe('task', () => { manifestManager.commit = jest.fn().mockResolvedValue(null); manifestManager.tryDispatch = jest.fn().mockResolvedValue([]); manifestManager.deleteArtifacts = jest.fn().mockResolvedValue([]); + manifestManager.cleanup = jest.fn().mockResolvedValue(null); await runTask(manifestManager); @@ -347,6 +362,7 @@ describe('task', () => { expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); expect(manifestManager.deleteArtifacts).toHaveBeenCalledWith([]); + expect(manifestManager.cleanup).toHaveBeenCalledWith(newManifest); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts index 6a89c50b86973f..a116311becfe5e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts @@ -174,6 +174,7 @@ export class ManifestTask { if (deleteErrors.length) { reportErrors(this.logger, deleteErrors); } + await manifestManager.cleanup(newManifest); } catch (err) { this.logger.error(wrapErrorIfNeeded(err)); } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts index 740068a836d057..2987049511c513 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts @@ -28,6 +28,12 @@ describe('artifact_client', () => { expect(fleetArtifactClient.listArtifacts).toHaveBeenCalled(); }); + test('can list artifact', async () => { + const response = await artifactClient.listArtifacts(); + expect(fleetArtifactClient.listArtifacts).toHaveBeenCalled(); + expect(response.items[0].id).toEqual('123'); + }); + test('can create artifact', async () => { const artifact = await getInternalArtifactMock('linux', 'v1'); await artifactClient.createArtifact(artifact); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts index c0930980dffb9f..ac19757e037b29 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts @@ -6,7 +6,12 @@ */ import { InternalArtifactCompleteSchema } from '../../schemas/artifacts'; -import { Artifact, ArtifactsClientInterface } from '../../../../../fleet/server'; +import { + Artifact, + ArtifactsClientInterface, + ListArtifactsProps, +} from '../../../../../fleet/server'; +import { ListResult } from '../../../../../fleet/common'; export interface EndpointArtifactClientInterface { getArtifact(id: string): Promise; @@ -14,6 +19,8 @@ export interface EndpointArtifactClientInterface { createArtifact(artifact: InternalArtifactCompleteSchema): Promise; deleteArtifact(id: string): Promise; + + listArtifacts(options?: ListArtifactsProps): Promise>; } /** @@ -49,6 +56,10 @@ export class EndpointArtifactClient implements EndpointArtifactClientInterface { return artifacts.items[0]; } + async listArtifacts(options?: ListArtifactsProps): Promise> { + return this.fleetArtifacts.listArtifacts(options); + } + async createArtifact( artifact: InternalArtifactCompleteSchema ): Promise { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index 89a704ecf1b1dd..d75e347b86bd51 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -1004,4 +1004,74 @@ describe('ManifestManager', () => { expect(context.packagePolicyService.update).toHaveBeenCalledTimes(2); }); }); + + describe('cleanup artifacts', () => { + const mockPolicyListIdsResponse = (items: string[]) => + jest.fn().mockResolvedValue({ + items, + page: 1, + per_page: 100, + total: items.length, + }); + + test('Successfully removes orphan artifacts', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({}); + context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]); + + context.savedObjectsClient.create = jest + .fn() + .mockImplementation((type: string, object: InternalManifestSchema) => ({ + attributes: object, + })); + const manifest = await manifestManager.buildNewManifest(); + + await manifestManager.cleanup(manifest); + const artifactToBeRemoved = await context.artifactClient.getArtifact(''); + expect(artifactToBeRemoved).not.toBeUndefined(); + + expect(context.artifactClient.deleteArtifact).toHaveBeenCalledWith( + getArtifactId(artifactToBeRemoved!) + ); + }); + + test('When there is no artifact to be removed', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({}); + context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]); + + context.savedObjectsClient.create = jest + .fn() + .mockImplementation((type: string, object: InternalManifestSchema) => ({ + attributes: object, + })); + + context.artifactClient.listArtifacts = jest.fn().mockResolvedValue([ + { + id: '123', + type: 'trustlist', + identifier: 'endpoint-trustlist-windows-v1', + packageName: 'endpoint', + encryptionAlgorithm: 'none', + relative_url: '/api/fleet/artifacts/trustlist-v1/d801aa1fb', + compressionAlgorithm: 'zlib', + decodedSha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decodedSize: 14, + encodedSha256: 'd29238d40', + encodedSize: 22, + body: 'eJyrVkrNKynKTC1WsoqOrQUAJxkFKQ==', + created: '2021-03-08T14:47:13.714Z', + }, + ]); + const manifest = await manifestManager.buildNewManifest(); + + await manifestManager.cleanup(manifest); + + expect(context.artifactClient.deleteArtifact).toHaveBeenCalledTimes(0); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 4c69aa1dd0737b..5c1d327b1b8928 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -5,9 +5,10 @@ * 2.0. */ +import pMap from 'p-map'; import semver from 'semver'; import LRU from 'lru-cache'; -import { isEqual } from 'lodash'; +import { isEqual, isEmpty } from 'lodash'; import { Logger, SavedObjectsClientContract } from 'src/core/server'; import { ListResult } from '../../../../../../fleet/common'; import { PackagePolicyServiceInterface } from '../../../../../../fleet/server'; @@ -515,4 +516,75 @@ export class ManifestManager { public getArtifactsClient(): EndpointArtifactClientInterface { return this.artifactClient; } + + /** + * Cleanup .fleet-artifacts index if there are some orphan artifacts + */ + public async cleanup(manifest: Manifest) { + try { + const fleetArtifacts = []; + const perPage = 100; + let page = 1; + + let fleetArtifactsResponse = await this.artifactClient.listArtifacts({ + perPage, + page, + }); + fleetArtifacts.push(...fleetArtifactsResponse.items); + + while ( + fleetArtifactsResponse.total > fleetArtifacts.length && + !isEmpty(fleetArtifactsResponse.items) + ) { + page += 1; + fleetArtifactsResponse = await this.artifactClient.listArtifacts({ + perPage, + page, + }); + fleetArtifacts.push(...fleetArtifactsResponse.items); + } + + if (isEmpty(fleetArtifacts)) { + return; + } + + const badArtifacts = []; + + const manifestArtifactsIds = manifest + .getAllArtifacts() + .map((artifact) => getArtifactId(artifact)); + + for (const fleetArtifact of fleetArtifacts) { + const artifactId = getArtifactId(fleetArtifact); + const isArtifactInManifest = manifestArtifactsIds.includes(artifactId); + + if (!isArtifactInManifest) { + badArtifacts.push(fleetArtifact); + } + } + + if (isEmpty(badArtifacts)) { + return; + } + + this.logger.error( + new EndpointError(`Cleaning up ${badArtifacts.length} orphan artifacts`, badArtifacts) + ); + + await pMap( + badArtifacts, + async (badArtifact) => this.artifactClient.deleteArtifact(getArtifactId(badArtifact)), + { + concurrency: 5, + /** When set to false, instead of stopping when a promise rejects, it will wait for all the promises to + * settle and then reject with an aggregated error containing all the errors from the rejected promises. */ + stopOnError: false, + } + ); + + this.logger.info(`All orphan artifacts has been removed successfully`); + } catch (error) { + this.logger.error(new EndpointError('There was an error cleaning orphan artifacts', error)); + } + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/mocks.ts index e2a4f9a3f53563..0a2e6253fd76fd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/mocks.ts @@ -47,6 +47,7 @@ export const createEndpointArtifactClientMock = ( const response = await endpointArtifactClient.createArtifact(...args); return response; }), + listArtifacts: jest.fn((...args) => endpointArtifactClientMocked.listArtifacts(...args)), getArtifact: jest.fn((...args) => endpointArtifactClientMocked.getArtifact(...args)), deleteArtifact: jest.fn((...args) => endpointArtifactClientMocked.deleteArtifact(...args)), _esClient: esClient, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b8d4d8b9f80500..2b58733c230492 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4182,12 +4182,8 @@ "inspector.view": "{viewName} を表示", "kibana_legacy.notify.fatalError.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}", "kibana_legacy.notify.fatalError.unavailableServerErrorMessage": "HTTP リクエストで接続に失敗しました。Kibana サーバーが実行されていて、ご使用のブラウザの接続が正常に動作していることを確認するか、システム管理者にお問い合わせください。", - "kibana_legacy.notify.toaster.errorMessage": "エラー:{errorMessage}\n {errorStack}", "kibana_legacy.notify.toaster.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}", "kibana_legacy.notify.toaster.unavailableServerErrorMessage": "HTTP リクエストで接続に失敗しました。Kibana サーバーが実行されていて、ご使用のブラウザの接続が正常に動作していることを確認するか、システム管理者にお問い合わせください。", - "kibana_legacy.paginate.controls.pageSizeLabel": "ページサイズ", - "kibana_legacy.paginate.controls.scrollTopButtonLabel": "最上部に移動", - "kibana_legacy.paginate.size.allDropDownOptionLabel": "すべて", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", @@ -6193,7 +6189,6 @@ "xpack.apm.alertAnnotationCriticalTitle": "重大アラート", "xpack.apm.alertAnnotationNoSeverityTitle": "アラート", "xpack.apm.alertAnnotationWarningTitle": "警告アラート", - "xpack.apm.alerting.fields.all_option": "すべて", "xpack.apm.alerting.fields.environment": "環境", "xpack.apm.alerting.fields.service": "サービス", "xpack.apm.alerting.fields.type": "型", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 17c6284ec9c6c6..3564f8b62f5cc1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4222,12 +4222,8 @@ "inspector.view": "视图:{viewName}", "kibana_legacy.notify.fatalError.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", "kibana_legacy.notify.fatalError.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", - "kibana_legacy.notify.toaster.errorMessage": "错误:{errorMessage}\n {errorStack}", "kibana_legacy.notify.toaster.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", "kibana_legacy.notify.toaster.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", - "kibana_legacy.paginate.controls.pageSizeLabel": "页面大小", - "kibana_legacy.paginate.controls.scrollTopButtonLabel": "滚动至顶部", - "kibana_legacy.paginate.size.allDropDownOptionLabel": "全部", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "已保存对象缺失", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "无法完全还原 URL,请确保使用共享功能。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,另外,似乎没有任何可安全删除的项目。\n\n通常,这可以通过移到全新的选项卡来解决,但这种情况可能是由更大的问题造成。如果您定期看到这个消息,请在 {gitHubIssuesUrl} 报告问题。", @@ -6243,7 +6239,6 @@ "xpack.apm.alertAnnotationCriticalTitle": "紧急告警", "xpack.apm.alertAnnotationNoSeverityTitle": "告警", "xpack.apm.alertAnnotationWarningTitle": "警告告警", - "xpack.apm.alerting.fields.all_option": "全部", "xpack.apm.alerting.fields.environment": "环境", "xpack.apm.alerting.fields.service": "服务", "xpack.apm.alerting.fields.type": "类型", diff --git a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts index 2742fbff294c0d..00b820a025c8b7 100644 --- a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts @@ -44,7 +44,7 @@ export default ({ getService }: FtrProviderContext) => { user: USER.ML_POWERUSER, expected: { responseCode: 200, - moduleIds: ['apm_jsbase', 'apm_transaction', 'apm_nodejs'], + moduleIds: ['apm_jsbase', 'apm_nodejs'], }, }, { diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index c4dd529ac14f59..6ff6b8113cb1a4 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -187,9 +187,11 @@ export default ({ getService }: FtrProviderContext) => { dashboards: [] as string[], }, }, + // Set startDatafeed and estimateModelMemory to false for the APM transaction test + // until there is a new data set available with metric data. { testTitleSuffix: - 'for apm_transaction with prefix, startDatafeed true and estimateModelMemory true', + 'for apm_transaction with prefix, startDatafeed false and estimateModelMemory false', sourceDataArchive: 'x-pack/test/functional/es_archives/ml/module_apm', indexPattern: { name: 'ft_module_apm', timeField: '@timestamp' }, module: 'apm_transaction', @@ -197,14 +199,14 @@ export default ({ getService }: FtrProviderContext) => { requestBody: { prefix: 'pf5_', indexPatternName: 'ft_module_apm', - startDatafeed: true, - end: Date.now(), + startDatafeed: false, + estimateModelMemory: false, }, expected: { responseCode: 200, jobs: [ { - jobId: 'pf5_high_mean_transaction_duration', + jobId: 'pf5_apm_metrics', jobState: JOB_STATE.CLOSED, datafeedState: DATAFEED_STATE.STOPPED, }, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 5ea5ad78d9479b..d402a74287f98e 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -137,6 +137,11 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./settings/custom_link')); }); + // suggestions + describe('suggestions', function () { + loadTestFile(require.resolve('./suggestions/suggestions')); + }); + // traces describe('traces/top_traces', function () { loadTestFile(require.resolve('./traces/top_traces')); diff --git a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.ts b/x-pack/test/apm_api_integration/tests/suggestions/suggestions.ts new file mode 100644 index 00000000000000..d551aec632fabc --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/suggestions/suggestions.ts @@ -0,0 +1,143 @@ +/* + * 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 { + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function suggestionsTests({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const archiveName = 'apm_8.0.0'; + + registry.when( + 'suggestions when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + describe('with environment', () => { + describe('with an empty string parameter', () => { + it('returns all environments', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: SERVICE_ENVIRONMENT, string: '' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "production", + "testing", + ], + } + `); + }); + }); + + describe('with a string parameter', () => { + it('returns items matching the string parameter', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: SERVICE_ENVIRONMENT, string: 'pr' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "production", + ], + } + `); + }); + }); + }); + + describe('with service name', () => { + describe('with an empty string parameter', () => { + it('returns all services', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: SERVICE_NAME, string: '' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "auditbeat", + "opbeans-dotnet", + "opbeans-go", + "opbeans-java", + "opbeans-node", + "opbeans-python", + "opbeans-ruby", + "opbeans-rum", + ], + } + `); + }); + }); + + describe('with a string parameter', () => { + it('returns items matching the string parameter', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: SERVICE_NAME, string: 'aud' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "auditbeat", + ], + } + `); + }); + }); + }); + + describe('with transaction type', () => { + describe('with an empty string parameter', () => { + it('returns all transaction types', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: TRANSACTION_TYPE, string: '' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "Worker", + "celery", + "page-load", + "request", + ], + } + `); + }); + }); + + describe('with a string parameter', () => { + it('returns items matching the string parameter', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: TRANSACTION_TYPE, string: 'w' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "Worker", + ], + } + `); + }); + }); + }); + } + ); +} diff --git a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts index 02819cd2615346..17b4fef06f5ce2 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts @@ -20,7 +20,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); const security = getService('security'); - describe('Ingest Pipelines', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/113439 + describe.skip('Ingest Pipelines', function () { this.tags('smoke'); before(async () => { await security.testUser.setRoles(['ingest_pipelines_user']); diff --git a/x-pack/test/functional/apps/lens/heatmap.ts b/x-pack/test/functional/apps/lens/heatmap.ts index 0655b09e84d566..ddc4130d388ce3 100644 --- a/x-pack/test/functional/apps/lens/heatmap.ts +++ b/x-pack/test/functional/apps/lens/heatmap.ts @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const elasticChart = getService('elasticChart'); const testSubjects = getService('testSubjects'); - describe('lens heatmap', () => { + // FLAKY: https://github.com/elastic/kibana/issues/113043 + describe.skip('lens heatmap', () => { before(async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); diff --git a/x-pack/test/functional/apps/lens/thresholds.ts b/x-pack/test/functional/apps/lens/thresholds.ts index bf6535acc7c8ea..10e330114442b3 100644 --- a/x-pack/test/functional/apps/lens/thresholds.ts +++ b/x-pack/test/functional/apps/lens/thresholds.ts @@ -64,5 +64,57 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Median of bytes', ]); }); + + it('should add a new group to the threshold layer when a right axis is enabled', async () => { + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + keepOpen: true, + }); + + await PageObjects.lens.changeAxisSide('right'); + + await PageObjects.lens.closeDimensionEditor(); + + await testSubjects.existOrFail('lnsXY_yThresholdRightPanel > lns-empty-dimension'); + }); + + it('should carry the style when moving a threshold to another group', async () => { + // style it enabling the fill + await testSubjects.click('lnsXY_yThresholdLeftPanel > lns-dimensionTrigger'); + await testSubjects.click('lnsXY_fill_below'); + await PageObjects.lens.closeDimensionEditor(); + + // drag and drop it to the left axis + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_yThresholdLeftPanel > lns-dimensionTrigger', + 'lnsXY_yThresholdRightPanel > lns-empty-dimension' + ); + + await testSubjects.click('lnsXY_yThresholdRightPanel > lns-dimensionTrigger'); + expect( + await find.existsByCssSelector('[data-test-subj="lnsXY_fill_below"][class$="isSelected"]') + ).to.be(true); + await PageObjects.lens.closeDimensionEditor(); + }); + + it('should duplicate also the original style when duplicating a threshold', async () => { + // drag and drop to the empty field to generate a duplicate + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_yThresholdRightPanel > lns-dimensionTrigger', + 'lnsXY_yThresholdRightPanel > lns-empty-dimension' + ); + + await ( + await find.byCssSelector( + '[data-test-subj="lnsXY_yThresholdRightPanel"]:nth-child(2) [data-test-subj="lns-dimensionTrigger"]' + ) + ).click(); + expect( + await find.existsByCssSelector('[data-test-subj="lnsXY_fill_below"][class$="isSelected"]') + ).to.be(true); + await PageObjects.lens.closeDimensionEditor(); + }); }); } diff --git a/x-pack/test/functional/services/observability/alerts.ts b/x-pack/test/functional/services/observability/alerts/common.ts similarity index 95% rename from x-pack/test/functional/services/observability/alerts.ts rename to x-pack/test/functional/services/observability/alerts/common.ts index 435da8ad94037a..7098fdec2a9d47 100644 --- a/x-pack/test/functional/services/observability/alerts.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -7,8 +7,8 @@ import querystring from 'querystring'; import { chunk } from 'lodash'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { WebElementWrapper } from '../../../../../../test/functional/services/lib/web_element_wrapper'; // Based on the x-pack/test/functional/es_archives/observability/alerts archive. const DATE_WITH_DATA = { @@ -19,12 +19,14 @@ const DATE_WITH_DATA = { const ALERTS_FLYOUT_SELECTOR = 'alertsFlyout'; const COPY_TO_CLIPBOARD_BUTTON_SELECTOR = 'copy-to-clipboard'; const ALERTS_TABLE_CONTAINER_SELECTOR = 'events-viewer-panel'; - const ACTION_COLUMN_INDEX = 1; type WorkflowStatus = 'open' | 'acknowledged' | 'closed'; -export function ObservabilityAlertsProvider({ getPageObjects, getService }: FtrProviderContext) { +export function ObservabilityAlertsCommonProvider({ + getPageObjects, + getService, +}: FtrProviderContext) { const testSubjects = getService('testSubjects'); const flyoutService = getService('flyout'); const pageObjects = getPageObjects(['common']); @@ -156,6 +158,7 @@ export function ObservabilityAlertsProvider({ getPageObjects, getService }: FtrP await actionsOverflowButton.click(); }; + // Workflow status const setWorkflowStatusForRow = async (rowIndex: number, workflowStatus: WorkflowStatus) => { await openActionsMenuForRow(rowIndex); diff --git a/x-pack/test/functional/services/observability/alerts/index.ts b/x-pack/test/functional/services/observability/alerts/index.ts new file mode 100644 index 00000000000000..f373b0d75c543b --- /dev/null +++ b/x-pack/test/functional/services/observability/alerts/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { ObservabilityAlertsPaginationProvider } from './pagination'; +import { ObservabilityAlertsCommonProvider } from './common'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export function ObservabilityAlertsProvider(context: FtrProviderContext) { + const common = ObservabilityAlertsCommonProvider(context); + const pagination = ObservabilityAlertsPaginationProvider(context); + + return { + common, + pagination, + }; +} diff --git a/x-pack/test/functional/services/observability/alerts/pagination.ts b/x-pack/test/functional/services/observability/alerts/pagination.ts new file mode 100644 index 00000000000000..6bffcf3596e2d7 --- /dev/null +++ b/x-pack/test/functional/services/observability/alerts/pagination.ts @@ -0,0 +1,109 @@ +/* + * 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'; + +const ROWS_PER_PAGE_SELECTOR = 'tablePaginationPopoverButton'; +const PREV_BUTTON_SELECTOR = 'pagination-button-previous'; +const NEXT_BUTTON_SELECTOR = 'pagination-button-next'; +const TEN_ROWS_SELECTOR = 'tablePagination-10-rows'; +const TWENTY_FIVE_ROWS_SELECTOR = 'tablePagination-25-rows'; +const FIFTY_ROWS_SELECTOR = 'tablePagination-50-rows'; +const BUTTON_ONE_SELECTOR = 'pagination-button-0'; +const BUTTON_TWO_SELECTOR = 'pagination-button-1'; + +export function ObservabilityAlertsPaginationProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + const getPageSizeSelector = async () => { + return await testSubjects.find(ROWS_PER_PAGE_SELECTOR); + }; + + const getPageSizeSelectorOrFail = async () => { + return await testSubjects.existOrFail(ROWS_PER_PAGE_SELECTOR); + }; + + const missingPageSizeSelectorOrFail = async () => { + return await testSubjects.missingOrFail(ROWS_PER_PAGE_SELECTOR); + }; + + const getTenRowsPageSelector = async () => { + return await testSubjects.find(TEN_ROWS_SELECTOR); + }; + + const getTwentyFiveRowsPageSelector = async () => { + return await testSubjects.find(TWENTY_FIVE_ROWS_SELECTOR); + }; + + const getFiftyRowsPageSelector = async () => { + return await testSubjects.find(FIFTY_ROWS_SELECTOR); + }; + + const getPrevPageButton = async () => { + return await testSubjects.find(PREV_BUTTON_SELECTOR); + }; + + const getPrevPageButtonOrFail = async () => { + return await testSubjects.existOrFail(PREV_BUTTON_SELECTOR); + }; + + const missingPrevPageButtonOrFail = async () => { + return await testSubjects.missingOrFail(PREV_BUTTON_SELECTOR); + }; + + const getNextPageButton = async () => { + return await testSubjects.find(NEXT_BUTTON_SELECTOR); + }; + + const getNextPageButtonOrFail = async () => { + return await testSubjects.existOrFail(NEXT_BUTTON_SELECTOR); + }; + + const getPaginationButtonOne = async () => { + return await testSubjects.find(BUTTON_ONE_SELECTOR); + }; + + const getPaginationButtonTwo = async () => { + return await testSubjects.find(BUTTON_TWO_SELECTOR); + }; + + const goToNextPage = async () => { + return await (await getNextPageButton()).click(); + }; + + const goToPrevPage = async () => { + return await (await getPrevPageButton()).click(); + }; + + const goToFirstPage = async () => { + await (await getPaginationButtonOne()).click(); + }; + + const getPrevButtonDisabledValue = async () => { + return await (await getPrevPageButton()).getAttribute('disabled'); + }; + + return { + getPageSizeSelector, + getPageSizeSelectorOrFail, + missingPageSizeSelectorOrFail, + getTenRowsPageSelector, + getTwentyFiveRowsPageSelector, + getFiftyRowsPageSelector, + getPrevPageButton, + getPrevPageButtonOrFail, + missingPrevPageButtonOrFail, + getNextPageButton, + getNextPageButtonOrFail, + getPaginationButtonOne, + getPaginationButtonTwo, + goToNextPage, + goToPrevPage, + goToFirstPage, + getPrevButtonDisabledValue, + }; +} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 88ba4c37559c57..adad80874dbc9f 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -103,8 +103,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('test.always-firing-SelectOption'); } - // FLAKY https://github.com/elastic/kibana/issues/112749 - describe.skip('create alert', function () { + describe('create alert', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('rulesTab'); @@ -236,6 +235,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Created rule "${alertName}"`); + await new Promise((resolve) => setTimeout(resolve, 1000)); await pageObjects.triggersActionsUI.searchAlerts(alertName); const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList(); expect(searchResultsAfterSave).to.eql([ diff --git a/x-pack/test/observability_functional/apps/observability/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/alerts/index.ts index 856d7e60996ec2..14019472eb2ca3 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/index.ts @@ -31,7 +31,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); - await observability.alerts.navigateToTimeWithData(); + await observability.alerts.common.navigateToTimeWithData(); }); after(async () => { @@ -40,50 +40,50 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Alerts table', () => { it('Renders the table', async () => { - await observability.alerts.getTableOrFail(); + await observability.alerts.common.getTableOrFail(); }); it('Renders the correct number of cells', async () => { await retry.try(async () => { - const cells = await observability.alerts.getTableCells(); + const cells = await observability.alerts.common.getTableCells(); expect(cells.length).to.be(TOTAL_ALERTS_CELL_COUNT); }); }); describe('Filtering', () => { afterEach(async () => { - await observability.alerts.clearQueryBar(); + await observability.alerts.common.clearQueryBar(); }); after(async () => { // NOTE: We do this as the query bar takes the place of the datepicker when it is in focus, so we'll reset // back to default. - await observability.alerts.submitQuery(''); + await observability.alerts.common.submitQuery(''); }); it('Autocompletion works', async () => { - await observability.alerts.typeInQueryBar('kibana.alert.s'); + await observability.alerts.common.typeInQueryBar('kibana.alert.s'); await testSubjects.existOrFail('autocompleteSuggestion-field-kibana.alert.start-'); await testSubjects.existOrFail('autocompleteSuggestion-field-kibana.alert.status-'); }); it('Applies filters correctly', async () => { - await observability.alerts.submitQuery('kibana.alert.status: recovered'); + await observability.alerts.common.submitQuery('kibana.alert.status: recovered'); await retry.try(async () => { - const cells = await observability.alerts.getTableCells(); + const cells = await observability.alerts.common.getTableCells(); expect(cells.length).to.be(RECOVERED_ALERTS_CELL_COUNT); }); }); it('Displays a no data state when filters produce zero results', async () => { - await observability.alerts.submitQuery('kibana.alert.consumer: uptime'); - await observability.alerts.getNoDataStateOrFail(); + await observability.alerts.common.submitQuery('kibana.alert.consumer: uptime'); + await observability.alerts.common.getNoDataStateOrFail(); }); }); describe('Date selection', () => { after(async () => { - await observability.alerts.navigateToTimeWithData(); + await observability.alerts.common.navigateToTimeWithData(); }); it('Correctly applies date picker selections', async () => { @@ -91,7 +91,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await (await testSubjects.find('superDatePickerToggleQuickMenuButton')).click(); // We shouldn't expect any data for the last 15 minutes await (await testSubjects.find('superDatePickerCommonlyUsed_Last_15 minutes')).click(); - await observability.alerts.getNoDataStateOrFail(); + await observability.alerts.common.getNoDataStateOrFail(); await pageObjects.common.waitUntilUrlIncludes('rangeFrom=now-15m&rangeTo=now'); }); }); @@ -99,37 +99,38 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Flyout', () => { it('Can be opened', async () => { - await observability.alerts.openAlertsFlyout(); - await observability.alerts.getAlertsFlyoutOrFail(); + await observability.alerts.common.openAlertsFlyout(); + await observability.alerts.common.getAlertsFlyoutOrFail(); }); it('Can be closed', async () => { - await observability.alerts.closeAlertsFlyout(); + await observability.alerts.common.closeAlertsFlyout(); await testSubjects.missingOrFail('alertsFlyout'); }); describe('When open', async () => { before(async () => { - await observability.alerts.openAlertsFlyout(); + await observability.alerts.common.openAlertsFlyout(); }); after(async () => { - await observability.alerts.closeAlertsFlyout(); + await observability.alerts.common.closeAlertsFlyout(); }); it('Displays the correct title', async () => { await retry.try(async () => { const titleText = await ( - await observability.alerts.getAlertsFlyoutTitle() + await observability.alerts.common.getAlertsFlyoutTitle() ).getVisibleText(); expect(titleText).to.contain('Log threshold'); }); }); it('Displays the correct content', async () => { - const flyoutTitles = await observability.alerts.getAlertsFlyoutDescriptionListTitles(); + const flyoutTitles = + await observability.alerts.common.getAlertsFlyoutDescriptionListTitles(); const flyoutDescriptions = - await observability.alerts.getAlertsFlyoutDescriptionListDescriptions(); + await observability.alerts.common.getAlertsFlyoutDescriptionListDescriptions(); const expectedTitles = [ 'Status', @@ -158,7 +159,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('Displays a View in App button', async () => { - await observability.alerts.getAlertsFlyoutViewInAppButtonOrFail(); + await observability.alerts.common.getAlertsFlyoutViewInAppButtonOrFail(); }); }); }); @@ -166,35 +167,35 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Cell actions', () => { beforeEach(async () => { await retry.try(async () => { - const cells = await observability.alerts.getTableCells(); + const cells = await observability.alerts.common.getTableCells(); const alertStatusCell = cells[2]; await alertStatusCell.moveMouseTo(); await retry.waitFor( 'cell actions visible', - async () => await observability.alerts.copyToClipboardButtonExists() + async () => await observability.alerts.common.copyToClipboardButtonExists() ); }); }); afterEach(async () => { - await observability.alerts.clearQueryBar(); + await observability.alerts.common.clearQueryBar(); }); it('Copy button works', async () => { // NOTE: We don't have access to the clipboard in a headless environment, // so we'll just check the button is clickable in the functional tests. - await (await observability.alerts.getCopyToClipboardButton()).click(); + await (await observability.alerts.common.getCopyToClipboardButton()).click(); }); it('Filter for value works', async () => { - await (await observability.alerts.getFilterForValueButton()).click(); + await (await observability.alerts.common.getFilterForValueButton()).click(); const queryBarValue = await ( - await observability.alerts.getQueryBar() + await observability.alerts.common.getQueryBar() ).getAttribute('value'); expect(queryBarValue).to.be('kibana.alert.status: "active"'); // Wait for request await retry.try(async () => { - const cells = await observability.alerts.getTableCells(); + const cells = await observability.alerts.common.getTableCells(); expect(cells.length).to.be(ACTIVE_ALERTS_CELL_COUNT); }); }); diff --git a/x-pack/test/observability_functional/apps/observability/alerts/pagination.ts b/x-pack/test/observability_functional/apps/observability/alerts/pagination.ts new file mode 100644 index 00000000000000..a00fbe2a77f346 --- /dev/null +++ b/x-pack/test/observability_functional/apps/observability/alerts/pagination.ts @@ -0,0 +1,130 @@ +/* + * 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'; + +const ROWS_NEEDED_FOR_PAGINATION = 10; +const DEFAULT_ROWS_PER_PAGE = 50; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + + // FAILING: https://github.com/elastic/kibana/issues/113486 + describe.skip('Observability alerts pagination', function () { + this.tags('includeFirefox'); + + const retry = getService('retry'); + const observability = getService('observability'); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + await observability.alerts.common.navigateToTimeWithData(); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + + describe(`When less than ${ROWS_NEEDED_FOR_PAGINATION} alerts are found`, () => { + before(async () => { + // current archiver has 3 closed alerts + await observability.alerts.common.setWorkflowStatusFilter('closed'); + }); + + after(async () => { + await observability.alerts.common.setWorkflowStatusFilter('open'); + }); + + it('Does not render page size selector', async () => { + await observability.alerts.pagination.missingPageSizeSelectorOrFail(); + }); + + it('Does not render pagination controls', async () => { + await observability.alerts.pagination.missingPrevPageButtonOrFail(); + }); + }); + + describe(`When ${ROWS_NEEDED_FOR_PAGINATION} alerts are found`, () => { + before(async () => { + // current archiver has 12 open alerts + await observability.alerts.common.setWorkflowStatusFilter('open'); + }); + + describe('Page size selector', () => { + it('Renders page size selector', async () => { + await observability.alerts.pagination.getPageSizeSelectorOrFail(); + }); + + it('Default rows per page is 50', async () => { + await retry.try(async () => { + const defaultAlertsPerPage = await ( + await observability.alerts.pagination.getPageSizeSelector() + ).getVisibleText(); + expect(defaultAlertsPerPage).to.contain(DEFAULT_ROWS_PER_PAGE); + }); + }); + + it('Shows up to 10 rows per page', async () => { + await retry.try(async () => { + await (await observability.alerts.pagination.getPageSizeSelector()).click(); + await (await observability.alerts.pagination.getTenRowsPageSelector()).click(); + const tableRows = await observability.alerts.common.getTableCellsInRows(); + expect(tableRows.length).to.not.be.greaterThan(10); + }); + }); + + it('Shows up to 25 rows per page', async () => { + await retry.try(async () => { + await (await observability.alerts.pagination.getPageSizeSelector()).click(); + await (await observability.alerts.pagination.getTwentyFiveRowsPageSelector()).click(); + const tableRows = await observability.alerts.common.getTableCellsInRows(); + expect(tableRows.length).to.not.be.greaterThan(25); + }); + }); + }); + + describe('Pagination controls', () => { + before(async () => { + await (await observability.alerts.pagination.getPageSizeSelector()).click(); + await (await observability.alerts.pagination.getTenRowsPageSelector()).click(); + }); + beforeEach(async () => { + await observability.alerts.pagination.goToFirstPage(); + }); + + it('Renders previous page button', async () => { + await observability.alerts.pagination.getPrevPageButtonOrFail(); + }); + + it('Renders next page button', async () => { + await observability.alerts.pagination.getNextPageButtonOrFail(); + }); + + it('Previous page button is disabled', async () => { + const prevButtonDisabledValue = + await observability.alerts.pagination.getPrevButtonDisabledValue(); + expect(prevButtonDisabledValue).to.be('true'); + }); + + it('Goes to next page', async () => { + await observability.alerts.pagination.goToNextPage(); + const tableRows = await observability.alerts.common.getTableCellsInRows(); + expect(tableRows.length).to.be(2); + }); + + it('Goes to previous page', async () => { + await (await observability.alerts.pagination.getPaginationButtonTwo()).click(); + await observability.alerts.pagination.goToPrevPage(); + const tableRows = await observability.alerts.common.getTableCellsInRows(); + + expect(tableRows.length).to.be(10); + }); + }); + }); + }); +}; diff --git a/x-pack/test/observability_functional/apps/observability/alerts/workflow_status.ts b/x-pack/test/observability_functional/apps/observability/alerts/workflow_status.ts index d491e239c6035b..a68636b8cb0c00 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/workflow_status.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/workflow_status.ts @@ -8,6 +8,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; +const OPEN_ALERTS_ROWS_COUNT = 12; + export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); @@ -19,7 +21,7 @@ export default ({ getService }: FtrProviderContext) => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); - await observability.alerts.navigateToTimeWithData(); + await observability.alerts.common.navigateToTimeWithData(); }); after(async () => { @@ -28,61 +30,61 @@ export default ({ getService }: FtrProviderContext) => { it('is filtered to only show "open" alerts by default', async () => { await retry.try(async () => { - const tableRows = await observability.alerts.getTableCellsInRows(); - expect(tableRows.length).to.be(12); + const tableRows = await observability.alerts.common.getTableCellsInRows(); + expect(tableRows.length).to.be(OPEN_ALERTS_ROWS_COUNT); }); }); it('can be set to "acknowledged" using the row menu', async () => { - await observability.alerts.setWorkflowStatusForRow(0, 'acknowledged'); + await observability.alerts.common.setWorkflowStatusForRow(0, 'acknowledged'); await retry.try(async () => { - const tableRows = await observability.alerts.getTableCellsInRows(); + const tableRows = await observability.alerts.common.getTableCellsInRows(); expect(tableRows.length).to.be(11); }); }); it('can be filtered to only show "acknowledged" alerts using the filter button', async () => { - await observability.alerts.setWorkflowStatusFilter('acknowledged'); + await observability.alerts.common.setWorkflowStatusFilter('acknowledged'); await retry.try(async () => { - const tableRows = await observability.alerts.getTableCellsInRows(); + const tableRows = await observability.alerts.common.getTableCellsInRows(); expect(tableRows.length).to.be(3); }); }); it('can be set to "closed" using the row menu', async () => { - await observability.alerts.setWorkflowStatusForRow(0, 'closed'); + await observability.alerts.common.setWorkflowStatusForRow(0, 'closed'); await retry.try(async () => { - const tableRows = await observability.alerts.getTableCellsInRows(); + const tableRows = await observability.alerts.common.getTableCellsInRows(); expect(tableRows.length).to.be(2); }); }); it('can be filtered to only show "closed" alerts using the filter button', async () => { - await observability.alerts.setWorkflowStatusFilter('closed'); + await observability.alerts.common.setWorkflowStatusFilter('closed'); await retry.try(async () => { - const tableRows = await observability.alerts.getTableCellsInRows(); + const tableRows = await observability.alerts.common.getTableCellsInRows(); expect(tableRows.length).to.be(4); }); }); it('can be set to "open" using the row menu', async () => { - await observability.alerts.setWorkflowStatusForRow(0, 'open'); + await observability.alerts.common.setWorkflowStatusForRow(0, 'open'); await retry.try(async () => { - const tableRows = await observability.alerts.getTableCellsInRows(); + const tableRows = await observability.alerts.common.getTableCellsInRows(); expect(tableRows.length).to.be(3); }); }); it('can be filtered to only show "open" alerts using the filter button', async () => { - await observability.alerts.setWorkflowStatusFilter('open'); + await observability.alerts.common.setWorkflowStatusFilter('open'); await retry.try(async () => { - const tableRows = await observability.alerts.getTableCellsInRows(); + const tableRows = await observability.alerts.common.getTableCellsInRows(); expect(tableRows.length).to.be(12); }); }); diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts index b823e1ee0869b8..019fb0994715ef 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./alerts')); loadTestFile(require.resolve('./alerts/workflow_status')); + loadTestFile(require.resolve('./alerts/pagination')); }); } diff --git a/yarn.lock b/yarn.lock index ca51f99108518a..ac54144a8fc11c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1397,10 +1397,10 @@ dependencies: "@elastic/ecs-helpers" "^1.1.0" -"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary.20": - version "8.0.0-canary.20" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.20.tgz#7e9826f693be409b83b3827d9eb14cb8326a5916" - integrity sha512-bma25umW+Wio6r10Ixhz/V2F10ZdMdBqNGjKRoOL9E5sU8CfyVSDL/A0j8Rt+FaY+JdagtF1EYT5ikVC4iRfLg== +"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary.21": + version "8.0.0-canary.21" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.21.tgz#6572a547071a17cf511a42fd93738780266a9f89" + integrity sha512-J/qRGYkTj+YeEJh5xci9eLlVPrfwSEURK/P+ZZ6ZKymFLz7VQvK1vvha2YJJBjpM3ERnLNDL0y/HTEjYkR3VtQ== dependencies: debug "^4.3.1" hpagent "^0.1.1" @@ -1430,10 +1430,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@37.6.0": - version "37.6.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-37.6.0.tgz#dc4fb2431e223047fa52b9035627c05a22197887" - integrity sha512-CeaKhwZCbTaq2h0cn3s9t0kGr+P+khVtYNa72zAVsH1Vhb6Ox0Z7cnvmn49qyjb3GnOHSYtQlbyrAcxqstfwbw== +"@elastic/eui@38.0.1": + version "38.0.1" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-38.0.1.tgz#de03c4436cd1b58327f54f57e4248183d681c0f8" + integrity sha512-yzV56rGVwhALnLVXsw2LGsFksh7c27BwIQOl2memDfm15VprzX5Fd+u6TpX8TkpFbTTgnYfyhI11W3eKVUYt1g== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" @@ -8362,7 +8362,7 @@ better-opn@^2.0.0: dependencies: open "^7.0.3" -big-integer@^1.6.16, big-integer@^1.6.48: +big-integer@^1.6.16: version "1.6.48" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== @@ -17523,6 +17523,11 @@ js-sha3@0.8.0: resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== +js-sql-parser@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/js-sql-parser/-/js-sql-parser-1.4.1.tgz#775516b3187dd5872ecec04bef8ed4a430242fda" + integrity sha512-J8zi3+/yK4FWSnVvLOjS2HIGfJhR6v7ApwIF8gZ/SpaO/tFIDlsgugD6ZMn6flXiuMsCjJxvhE0+xBgbdzvDDw== + js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -20096,13 +20101,6 @@ node-sass@^6.0.1: stdout-stream "^1.4.0" "true-case-path" "^1.0.2" -node-sql-parser@^3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/node-sql-parser/-/node-sql-parser-3.6.1.tgz#6f096e9df1f19d1e2daa658d864bd68b0e2cd2c6" - integrity sha512-AseDvELmUvL22L6C63DsTuzF+0i/HBIHjJq/uxC7jV3PGpAUib5Oe6oz4sgAniSUMPSZQbZmRore6Na68Sg4Tg== - dependencies: - big-integer "^1.6.48" - nodemailer@^6.6.2: version "6.6.2" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.6.2.tgz#e184c9ed5bee245a3e0bcabc7255866385757114"