diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d3706a27d1ec5..c398316e634b9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -24,7 +24,6 @@ /x-pack/plugins/lens/ @elastic/kibana-vis-editors /src/plugins/advanced_settings/ @elastic/kibana-vis-editors /src/plugins/charts/ @elastic/kibana-vis-editors -/src/plugins/kibana_legacy/ @elastic/kibana-vis-editors /src/plugins/vis_default_editor/ @elastic/kibana-vis-editors /src/plugins/vis_types/metric/ @elastic/kibana-vis-editors /src/plugins/vis_types/table/ @elastic/kibana-vis-editors diff --git a/.i18nrc.json b/.i18nrc.json index 5f88430c4c551..9485f5b9b84e7 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -47,7 +47,6 @@ "indexPatternManagement": "src/plugins/data_view_management", "interactiveSetup": "src/plugins/interactive_setup", "advancedSettings": "src/plugins/advanced_settings", - "kibana_legacy": "src/plugins/kibana_legacy", "kibanaOverview": "src/plugins/kibana_overview", "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 7ee0945a5e639..e997c0bc68cde 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -166,10 +166,6 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel. |The plugin provides UI and APIs for the interactive setup mode. -|{kib-repo}blob/{branch}/src/plugins/kibana_legacy/README.md[kibanaLegacy] -|This plugin contains several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. - - |{kib-repo}blob/{branch}/src/plugins/kibana_overview/README.md[kibanaOverview] |An overview page highlighting Kibana apps diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 7eb609c290fec..41c4d3bdd1b35 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -29,7 +29,6 @@ pageLoadAssetSize: ingestPipelines: 58003 inputControlVis: 172675 inspector: 148711 - kibanaLegacy: 107711 kibanaOverview: 56279 lens: 96624 licenseManagement: 41817 diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 26725aff71b6c..348b5c4af3e58 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -16,6 +16,7 @@ import { getServerOptions, getRequestId, } from '@kbn/server-http-tools'; +import agent from 'elastic-apm-node'; import type { Duration } from 'moment'; import { Observable } from 'rxjs'; @@ -345,6 +346,9 @@ export class HttpServer { ...(request.app ?? {}), requestId, requestUuid: uuid.v4(), + // Kibana stores trace.id until https://github.com/elastic/apm-agent-nodejs/issues/2353 is resolved + // The current implementation of the APM agent ends a request transaction before "response" log is emitted. + traceId: agent.currentTraceIds['trace.id'], } as KibanaRequestState; return responseToolkit.continue; }); diff --git a/src/core/server/http/logging/get_response_log.test.ts b/src/core/server/http/logging/get_response_log.test.ts index df91ae9c1a98b..4e749e9b6c8e0 100644 --- a/src/core/server/http/logging/get_response_log.test.ts +++ b/src/core/server/http/logging/get_response_log.test.ts @@ -27,6 +27,7 @@ interface RequestFixtureOptions { path?: string; query?: Record; response?: Record | Boom.Boom; + app?: Record; } function createMockHapiRequest({ @@ -39,6 +40,7 @@ function createMockHapiRequest({ path = '/path', query = {}, response = { headers: {}, statusCode: 200 }, + app = {}, }: RequestFixtureOptions = {}): Request { return { auth, @@ -50,6 +52,7 @@ function createMockHapiRequest({ path, query, response, + app, } as unknown as Request; } @@ -143,6 +146,17 @@ describe('getEcsResponseLog', () => { expect(result.message).toMatchInlineSnapshot(`"GET /path 200"`); }); + test('set traceId stored in the request app storage', () => { + const req = createMockHapiRequest({ + app: { + foo: 'bar', + traceId: 'trace_id', + }, + }); + const result = getEcsResponseLog(req, logger); + expect(result.meta?.trace?.id).toBe('trace_id'); + }); + test('handles Boom errors in the response', () => { const req = createMockHapiRequest({ response: Boom.badRequest(), @@ -280,6 +294,7 @@ describe('getEcsResponseLog', () => { "status_code": 200, }, }, + "trace": undefined, "url": Object { "path": "/path", "query": "", diff --git a/src/core/server/http/logging/get_response_log.ts b/src/core/server/http/logging/get_response_log.ts index 37ee618e43395..e65871c1b1f11 100644 --- a/src/core/server/http/logging/get_response_log.ts +++ b/src/core/server/http/logging/get_response_log.ts @@ -13,6 +13,7 @@ import numeral from '@elastic/numeral'; import { LogMeta } from '@kbn/logging'; import { Logger } from '../../logging'; import { getResponsePayloadBytes } from './get_payload_size'; +import type { KibanaRequestState } from '../router'; const FORBIDDEN_HEADERS = ['authorization', 'cookie', 'set-cookie']; const REDACTED_HEADER_TEXT = '[REDACTED]'; @@ -65,6 +66,8 @@ export function getEcsResponseLog(request: Request, log: Logger) { const bytes = getResponsePayloadBytes(response, log); const bytesMsg = bytes ? ` - ${numeral(bytes).format('0.0b')}` : ''; + const traceId = (request.app as KibanaRequestState).traceId; + const meta: LogMeta = { client: { ip: request.info.remoteAddress, @@ -95,6 +98,7 @@ export function getEcsResponseLog(request: Request, log: Logger) { user_agent: { original: request.headers['user-agent'], }, + trace: traceId ? { id: traceId } : undefined, }; return { diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 89511c00a8f32..94d353e1335b3 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -35,6 +35,7 @@ export interface KibanaRequestState extends RequestApplicationState { requestId: string; requestUuid: string; rewrittenUrl?: URL; + traceId?: string; } /** diff --git a/src/core/server/logging/layouts/json_layout.test.ts b/src/core/server/logging/layouts/json_layout.test.ts index d3bf2eab473a4..259900fd5d3fb 100644 --- a/src/core/server/logging/layouts/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -343,9 +343,9 @@ test('format() meta can not override tracing properties', () => { trace: { id: 'trace_override' }, transaction: { id: 'transaction_override' }, }, - spanId: 'spanId-1', - traceId: 'traceId-1', - transactionId: 'transactionId-1', + spanId: 'spanId', + traceId: 'traceId', + transactionId: 'transactionId', }) ) ).toStrictEqual({ @@ -359,8 +359,8 @@ test('format() meta can not override tracing properties', () => { process: { pid: 3, }, - span: { id: 'spanId-1' }, - trace: { id: 'traceId-1' }, - transaction: { id: 'transactionId-1' }, + span: { id: 'span_override' }, + trace: { id: 'trace_override' }, + transaction: { id: 'transaction_override' }, }); }); diff --git a/src/core/server/logging/layouts/json_layout.ts b/src/core/server/logging/layouts/json_layout.ts index 5c23e7ac1a911..84ceb0a30e9bb 100644 --- a/src/core/server/logging/layouts/json_layout.ts +++ b/src/core/server/logging/layouts/json_layout.ts @@ -42,6 +42,10 @@ export class JsonLayout implements Layout { } public format(record: LogRecord): string { + const spanId = record.meta?.span?.id ?? record.spanId; + const traceId = record.meta?.trace?.id ?? record.traceId; + const transactionId = record.meta?.transaction?.id ?? record.transactionId; + const log: Ecs = { ecs: { version: '8.0.0' }, '@timestamp': moment(record.timestamp).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), @@ -54,9 +58,9 @@ export class JsonLayout implements Layout { process: { pid: record.pid, }, - span: record.spanId ? { id: record.spanId } : undefined, - trace: record.traceId ? { id: record.traceId } : undefined, - transaction: record.transactionId ? { id: record.transactionId } : undefined, + span: spanId ? { id: spanId } : undefined, + trace: traceId ? { id: traceId } : undefined, + transaction: transactionId ? { id: transactionId } : undefined, }; const output = record.meta ? merge({ ...record.meta }, log) : log; diff --git a/src/plugins/kibana_legacy/README.md b/src/plugins/kibana_legacy/README.md deleted file mode 100644 index d66938cca6d13..0000000000000 --- a/src/plugins/kibana_legacy/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# kibana-legacy - -This plugin contains several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. - -This plugin will be removed once all parts of legacy Kibana are removed from other plugins. - -All of this plugin should be considered deprecated. New code should never integrate with the services provided from this plugin. \ No newline at end of file diff --git a/src/plugins/kibana_legacy/jest.config.js b/src/plugins/kibana_legacy/jest.config.js deleted file mode 100644 index a2bdf5649f900..0000000000000 --- a/src/plugins/kibana_legacy/jest.config.js +++ /dev/null @@ -1,16 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/kibana_legacy'], - coverageDirectory: '/target/kibana-coverage/jest/src/plugins/kibana_legacy', - coverageReporters: ['text', 'html'], - collectCoverageFrom: ['/src/plugins/kibana_legacy/public/**/*.{js,ts,tsx}'], -}; diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json deleted file mode 100644 index afca886ad9376..0000000000000 --- a/src/plugins/kibana_legacy/kibana.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "kibanaLegacy", - "version": "kibana", - "server": false, - "ui": true, - "owner": { - "name": "Vis Editors", - "githubTeam": "kibana-vis-editors" - } -} diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts deleted file mode 100644 index 2acb501a7262f..0000000000000 --- a/src/plugins/kibana_legacy/public/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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. - */ - -// TODO: https://github.com/elastic/kibana/issues/110891 -/* eslint-disable @kbn/eslint/no_export_all */ - -import { KibanaLegacyPlugin } from './plugin'; - -export const plugin = () => new KibanaLegacyPlugin(); - -export * from './plugin'; -export * from './notify'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts deleted file mode 100644 index 3eac98a84d40a..0000000000000 --- a/src/plugins/kibana_legacy/public/mocks.ts +++ /dev/null @@ -1,23 +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 { KibanaLegacyPlugin } from './plugin'; - -export type Setup = jest.Mocked>; -export type Start = jest.Mocked>; - -const createSetupContract = (): Setup => ({}); - -const createStartContract = (): Start => ({ - loadFontAwesome: jest.fn(), -}); - -export const kibanaLegacyPluginMock = { - createSetupContract, - createStartContract, -}; diff --git a/src/plugins/kibana_legacy/public/notify/index.ts b/src/plugins/kibana_legacy/public/notify/index.ts deleted file mode 100644 index d4dcaa77cc47a..0000000000000 --- a/src/plugins/kibana_legacy/public/notify/index.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 * from './lib'; diff --git a/src/plugins/kibana_legacy/public/notify/lib/format_es_msg.test.js b/src/plugins/kibana_legacy/public/notify/lib/format_es_msg.test.js deleted file mode 100644 index d99db9f19f32e..0000000000000 --- a/src/plugins/kibana_legacy/public/notify/lib/format_es_msg.test.js +++ /dev/null @@ -1,65 +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 { formatESMsg } from './format_es_msg'; -import expect from '@kbn/expect'; - -describe('formatESMsg', () => { - test('should return undefined if passed a basic error', () => { - const err = new Error('This is a normal error'); - - const actual = formatESMsg(err); - - expect(actual).to.be(undefined); - }); - - test('should return undefined if passed a string', () => { - const err = 'This is a error string'; - - const actual = formatESMsg(err); - - expect(actual).to.be(undefined); - }); - - test('should return the root_cause if passed an extended elasticsearch', () => { - const err = new Error('This is an elasticsearch error'); - err.resp = { - error: { - root_cause: [ - { - reason: 'I am the detailed message', - }, - ], - }, - }; - - const actual = formatESMsg(err); - - expect(actual).to.equal('I am the detailed message'); - }); - - test('should combine the reason messages if more than one is returned.', () => { - const err = new Error('This is an elasticsearch error'); - err.resp = { - error: { - root_cause: [ - { - reason: 'I am the detailed message 1', - }, - { - reason: 'I am the detailed message 2', - }, - ], - }, - }; - - const actual = formatESMsg(err); - - expect(actual).to.equal('I am the detailed message 1\nI am the detailed message 2'); - }); -}); diff --git a/src/plugins/kibana_legacy/public/notify/lib/format_es_msg.ts b/src/plugins/kibana_legacy/public/notify/lib/format_es_msg.ts deleted file mode 100644 index 9c86899209a9c..0000000000000 --- a/src/plugins/kibana_legacy/public/notify/lib/format_es_msg.ts +++ /dev/null @@ -1,26 +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 _ from 'lodash'; - -const getRootCause = (err: Record | string) => _.get(err, 'resp.error.root_cause'); - -/** - * Utilize the extended error information returned from elasticsearch - * @param {Error|String} err - * @returns {string} - */ -export const formatESMsg = (err: Record | string) => { - const rootCause = getRootCause(err); - - if (!Array.isArray(rootCause)) { - return; - } - - return rootCause.map((cause: Record) => cause.reason).join('\n'); -}; diff --git a/src/plugins/kibana_legacy/public/notify/lib/index.ts b/src/plugins/kibana_legacy/public/notify/lib/index.ts deleted file mode 100644 index 7f1cfb0e5b1fe..0000000000000 --- a/src/plugins/kibana_legacy/public/notify/lib/index.ts +++ /dev/null @@ -1,10 +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 { formatESMsg } from './format_es_msg'; -export { formatMsg } from './format_msg'; diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts deleted file mode 100644 index a154770bbfffd..0000000000000 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ /dev/null @@ -1,30 +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 type { CoreSetup } from 'kibana/public'; - -export class KibanaLegacyPlugin { - public setup(core: CoreSetup<{}, KibanaLegacyStart>) { - return {}; - } - - public start() { - return { - /** - * Loads the font-awesome icon font. Should be removed once the last consumer has migrated to EUI - * @deprecated - */ - loadFontAwesome: async () => { - await import('./font_awesome'); - }, - }; - } -} - -export type KibanaLegacySetup = ReturnType; -export type KibanaLegacyStart = ReturnType; diff --git a/src/plugins/kibana_legacy/tsconfig.json b/src/plugins/kibana_legacy/tsconfig.json deleted file mode 100644 index 17f1a70838fd7..0000000000000 --- a/src/plugins/kibana_legacy/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["../../../typings/**/*", "public/**/*", "server/**/*", "config.ts"], - "references": [{ "path": "../../core/tsconfig.json" }] -} diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index 0f00f12e71c20..1e2b2e57d51c8 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -16,7 +16,6 @@ { "path": "../kibana_utils/tsconfig.json" }, { "path": "../usage_collection/tsconfig.json" }, { "path": "../telemetry/tsconfig.json" }, - { "path": "../kibana_legacy/tsconfig.json"}, { "path": "../ui_actions/tsconfig.json" }, { "path": "../expressions/tsconfig.json" }, { "path": "../home/tsconfig.json" }, diff --git a/src/plugins/url_forwarding/tsconfig.json b/src/plugins/url_forwarding/tsconfig.json index c6ef2a0286da1..464cca51c6b9f 100644 --- a/src/plugins/url_forwarding/tsconfig.json +++ b/src/plugins/url_forwarding/tsconfig.json @@ -9,6 +9,5 @@ "include": ["public/**/*"], "references": [ { "path": "../../core/tsconfig.json" }, - { "path": "../kibana_legacy/tsconfig.json" } ] } diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index 80fbd864fd815..d5cd423b2b123 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -267,6 +267,14 @@ export function TransactionDistributionChart({ yAccessors={['doc_count']} color={areaSeriesColors[i]} fit="lookahead" + // To make the area appear without the orphaned points technique, + // we changed the original data to replace values of 0 with 0.0001. + // To show the correct values again in tooltips, we use a custom tickFormat to round values. + // We can safely do this because all transaction values above 0 are without decimal points anyway. + // An update for Elastic Charts is in the works to be able to customize the above "fit" + // attribute. Once that is available we can get rid of the full workaround. + // Elastic Charts issue: https://github.com/elastic/elastic-charts/issues/1489 + tickFormat={(p) => `${Math.round(p)}`} /> ))} diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 2fd312502a3c7..a00dd94ce346c 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -33,7 +33,6 @@ "requiredBundles": [ "discover", "home", - "kibanaLegacy", "kibanaReact", "kibanaUtils", "lens", diff --git a/src/plugins/kibana_legacy/public/notify/lib/format_msg.test.js b/x-pack/plugins/canvas/public/lib/format_msg.test.ts similarity index 50% rename from src/plugins/kibana_legacy/public/notify/lib/format_msg.test.js rename to x-pack/plugins/canvas/public/lib/format_msg.test.ts index 30061141ee46d..0643e2a6f2b95 100644 --- a/src/plugins/kibana_legacy/public/notify/lib/format_msg.test.js +++ b/x-pack/plugins/canvas/public/lib/format_msg.test.ts @@ -1,13 +1,12 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ -import { formatMsg } from './format_msg'; import expect from '@kbn/expect'; +import { formatMsg, formatESMsg } from './format_msg'; describe('formatMsg', () => { test('should prepend the second argument to result', () => { @@ -65,4 +64,59 @@ describe('formatMsg', () => { expect(actual).to.equal('I am the detailed message'); }); + + describe('formatESMsg', () => { + test('should return undefined if passed a basic error', () => { + const err = new Error('This is a normal error'); + + const actual = formatESMsg(err); + + expect(actual).to.be(undefined); + }); + + test('should return undefined if passed a string', () => { + const err = 'This is a error string'; + + const actual = formatESMsg(err); + + expect(actual).to.be(undefined); + }); + + test('should return the root_cause if passed an extended elasticsearch', () => { + const err: Record = new Error('This is an elasticsearch error'); + err.resp = { + error: { + root_cause: [ + { + reason: 'I am the detailed message', + }, + ], + }, + }; + + const actual = formatESMsg(err); + + expect(actual).to.equal('I am the detailed message'); + }); + + test('should combine the reason messages if more than one is returned.', () => { + const err: Record = new Error('This is an elasticsearch error'); + err.resp = { + error: { + root_cause: [ + { + reason: 'I am the detailed message 1', + }, + { + reason: 'I am the detailed message 2', + }, + ], + }, + }; + + const actual = formatESMsg(err); + + expect(actual).to.equal('I am the detailed message 1\nI am the detailed message 2'); + }); + }); }); diff --git a/src/plugins/kibana_legacy/public/notify/lib/format_msg.ts b/x-pack/plugins/canvas/public/lib/format_msg.ts similarity index 71% rename from src/plugins/kibana_legacy/public/notify/lib/format_msg.ts rename to x-pack/plugins/canvas/public/lib/format_msg.ts index 53fe0ba800a27..c996fe6c890be 100644 --- a/src/plugins/kibana_legacy/public/notify/lib/format_msg.ts +++ b/x-pack/plugins/canvas/public/lib/format_msg.ts @@ -1,16 +1,31 @@ /* * 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. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { formatESMsg } from './format_es_msg'; const has = _.has; +const getRootCause = (err: Record | string) => _.get(err, 'resp.error.root_cause'); + +/** + * Utilize the extended error information returned from elasticsearch + * @param {Error|String} err + * @returns {string} + */ +export const formatESMsg = (err: Record | string) => { + const rootCause = getRootCause(err); + + if (!Array.isArray(rootCause)) { + return; + } + + return rootCause.map((cause: Record) => cause.reason).join('\n'); +}; + /** * Formats the error message from an error object, extended elasticsearch * object or simple string; prepends optional second parameter to the message @@ -36,14 +51,14 @@ export function formatMsg(err: Record | string, source: string = '' // is an Angular $http "error object" if (err.status === -1) { // status = -1 indicates that the request was failed to reach the server - message += i18n.translate('kibana_legacy.notify.toaster.unavailableServerErrorMessage', { + message += i18n.translate('xpack.canvas.formatMsg.toaster.unavailableServerErrorMessage', { defaultMessage: 'An HTTP request has failed to connect. ' + 'Please check if the Kibana server is running and that your browser has a working connection, ' + 'or contact your system administrator.', }); } else { - message += i18n.translate('kibana_legacy.notify.toaster.errorStatusMessage', { + message += i18n.translate('xpack.canvas.formatMsg.toaster.errorStatusMessage', { defaultMessage: 'Error {errStatus} {errStatusText}: {errMessage}', values: { errStatus: err.status, diff --git a/x-pack/plugins/canvas/public/services/kibana/notify.ts b/x-pack/plugins/canvas/public/services/kibana/notify.ts index 1752840127fe1..22d4b6f8a476d 100644 --- a/x-pack/plugins/canvas/public/services/kibana/notify.ts +++ b/x-pack/plugins/canvas/public/services/kibana/notify.ts @@ -8,7 +8,7 @@ import { get } from 'lodash'; import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; -import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public'; +import { formatMsg } from '../../lib/format_msg'; import { ToastInputFields } from '../../../../../../src/core/public'; import { CanvasStartDeps } from '../../plugin'; import { CanvasNotifyService } from '../notify'; diff --git a/x-pack/plugins/canvas/storybook/canvas.webpack.ts b/x-pack/plugins/canvas/storybook/canvas.webpack.ts index f4980741cc3e2..db59af20440e2 100644 --- a/x-pack/plugins/canvas/storybook/canvas.webpack.ts +++ b/x-pack/plugins/canvas/storybook/canvas.webpack.ts @@ -45,7 +45,6 @@ export const canvasWebpack = { test: [ resolve(KIBANA_ROOT, 'x-pack/plugins/canvas/public/components/embeddable_flyout'), resolve(KIBANA_ROOT, 'x-pack/plugins/reporting/public'), - resolve(KIBANA_ROOT, 'src/plugins/kibana_legacy/public/paginate'), ], use: 'null-loader', }, diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 5064bac975a9c..f0dd93fa0f7a0 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -39,7 +39,6 @@ { "path": "../../../src/plugins/expression_shape/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, { "path": "../../../src/plugins/inspector/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_legacy/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/presentation_util/tsconfig.json" }, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index b6f236852f940..4d5552a17fb53 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -454,7 +454,7 @@ export function Detail() { name: ( ), isSelected: panel === 'policies', diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 425781e81483d..329deb37f36d6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -211,7 +211,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps { field: 'packagePolicy.name', name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', { - defaultMessage: 'Integration Policy', + defaultMessage: 'Integration policy', }), render(_, { packagePolicy }) { return ; @@ -269,7 +269,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps { field: 'packagePolicy.updated_by', name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', { - defaultMessage: 'Last Updated By', + defaultMessage: 'Last updated by', }), truncateText: true, render(updatedBy) { @@ -279,7 +279,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps { field: 'packagePolicy.updated_at', name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedAt', { - defaultMessage: 'Last Updated', + defaultMessage: 'Last updated', }), truncateText: true, render(updatedAt: InMemoryPackagePolicyAndAgentPolicy['packagePolicy']['updated_at']) { diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json index 03729c706df25..e8b651a9eb0ea 100644 --- a/x-pack/plugins/graph/kibana.json +++ b/x-pack/plugins/graph/kibana.json @@ -8,8 +8,7 @@ "licensing", "data", "navigation", - "savedObjects", - "kibanaLegacy" + "savedObjects" ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index fc6c6170509d9..5a7f931538bf6 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -25,9 +25,9 @@ import { LicensingPluginStart } from '../../licensing/public'; import { checkLicense } from '../common/check_license'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; -import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; import './index.scss'; +import('./font_awesome'); import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; import { GraphSavePolicy } from './types'; import { graphRouter } from './router'; @@ -60,18 +60,16 @@ export interface GraphDependencies { graphSavePolicy: GraphSavePolicy; overlays: OverlayStart; savedObjects: SavedObjectsStart; - kibanaLegacy: KibanaLegacyStart; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; uiSettings: IUiSettingsClient; history: ScopedHistory; spaces?: SpacesApi; } -export type GraphServices = Omit; +export type GraphServices = Omit; -export const renderApp = ({ history, kibanaLegacy, element, ...deps }: GraphDependencies) => { +export const renderApp = ({ history, element, ...deps }: GraphDependencies) => { const { chrome, capabilities } = deps; - kibanaLegacy.loadFontAwesome(); if (!capabilities.graph.save) { chrome.setBadge({ diff --git a/src/plugins/kibana_legacy/public/font_awesome/font_awesome.scss b/x-pack/plugins/graph/public/font_awesome/font_awesome.scss similarity index 100% rename from src/plugins/kibana_legacy/public/font_awesome/font_awesome.scss rename to x-pack/plugins/graph/public/font_awesome/font_awesome.scss diff --git a/src/plugins/kibana_legacy/public/font_awesome/index.ts b/x-pack/plugins/graph/public/font_awesome/index.ts similarity index 50% rename from src/plugins/kibana_legacy/public/font_awesome/index.ts rename to x-pack/plugins/graph/public/font_awesome/index.ts index 3a2a6fb94dd9b..162622c9dc526 100644 --- a/src/plugins/kibana_legacy/public/font_awesome/index.ts +++ b/x-pack/plugins/graph/public/font_awesome/index.ts @@ -1,9 +1,8 @@ /* * 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. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import './font_awesome.scss'; diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index a1bc8a93f7f7a..1782f8202c415 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -20,7 +20,6 @@ import { } from '../../../../src/core/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; -import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; @@ -43,7 +42,6 @@ export interface GraphPluginStartDependencies { licensing: LicensingPluginStart; data: DataPublicPluginStart; savedObjects: SavedObjectsStart; - kibanaLegacy: KibanaLegacyStart; home?: HomePublicPluginStart; spaces?: SpacesApi; } @@ -99,7 +97,6 @@ export class GraphPlugin coreStart, navigation: pluginsStart.navigation, data: pluginsStart.data, - kibanaLegacy: pluginsStart.kibanaLegacy, savedObjectsClient: coreStart.savedObjects.client, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, diff --git a/x-pack/plugins/graph/tsconfig.json b/x-pack/plugins/graph/tsconfig.json index 6a5623b311d5e..bd7e4907ed4e4 100644 --- a/x-pack/plugins/graph/tsconfig.json +++ b/x-pack/plugins/graph/tsconfig.json @@ -21,7 +21,6 @@ { "path": "../../../src/plugins/data/tsconfig.json"}, { "path": "../../../src/plugins/navigation/tsconfig.json" }, { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, - { "path": "../../../src/plugins/kibana_legacy/tsconfig.json"}, { "path": "../../../src/plugins/home/tsconfig.json"}, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 9ed05bbdc2edf..577e41b8816cd 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -16,7 +16,6 @@ "share", "embeddable", "uiActions", - "kibanaLegacy", "discover", "triggersActionsUi", "fieldFormats" diff --git a/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts b/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts index 4bbbe10df7f6c..42cbb4bf3485b 100644 --- a/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts +++ b/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts @@ -7,7 +7,6 @@ import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; -import { kibanaLegacyPluginMock } from '../../../../../src/plugins/kibana_legacy/public/mocks'; import { sharePluginMock } from '../../../../../src/plugins/share/public/mocks'; import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks'; import { triggersActionsUiMock } from '../../../triggers_actions_ui/public/mocks'; @@ -15,7 +14,6 @@ import { triggersActionsUiMock } from '../../../triggers_actions_ui/public/mocks export const createMlStartDepsMock = () => ({ data: dataPluginMock.createStartContract(), share: sharePluginMock.createStartContract(), - kibanaLegacy: kibanaLegacyPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), spaces: jest.fn(), embeddable: embeddablePluginMock.createStartContract(), diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index e5346b6618098..59419303d7a6f 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -25,7 +25,6 @@ import type { SpacesPluginStart } from '../../spaces/public'; import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import type { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; -import type { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; import type { LicenseManagementUIPluginSetup } from '../../license_management/public'; import type { LicensingPluginSetup } from '../../licensing/public'; @@ -54,7 +53,6 @@ import type { export interface MlStartDependencies { data: DataPublicPluginStart; share: SharePluginStart; - kibanaLegacy: KibanaLegacyStart; uiActions: UiActionsStart; spaces?: SpacesPluginStart; embeddable: EmbeddableStart; @@ -109,7 +107,6 @@ export class MlPlugin implements Plugin { { data: pluginsStart.data, share: pluginsStart.share, - kibanaLegacy: pluginsStart.kibanaLegacy, security: pluginsSetup.security, licensing: pluginsSetup.licensing, management: pluginsSetup.management, diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index bc0cf47181585..d10d8d674fcaf 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -24,7 +24,6 @@ "kibanaUtils", "home", "alerting", - "kibanaReact", - "kibanaLegacy" + "kibanaReact" ] } diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx b/x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx index b4c2a4e86d374..4da427156c19d 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx +++ b/x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx @@ -10,7 +10,7 @@ import { includes } from 'lodash'; import { IHttpFetchError, ResponseErrorBody } from 'kibana/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; -import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public'; +import { formatMsg } from '../../lib/format_msg'; import { toMountPoint, useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { MonitoringStartPluginDependencies } from '../../types'; diff --git a/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx index 33bf10d59fb42..2622a6c9e553d 100644 --- a/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx +++ b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx @@ -10,7 +10,7 @@ import { includes } from 'lodash'; import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Legacy } from '../legacy_shims'; -import { formatMsg } from '../../../../../src/plugins/kibana_legacy/public'; +import { formatMsg } from './format_msg'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; export function formatMonitoringError(err: any) { diff --git a/x-pack/plugins/monitoring/public/lib/format_msg.test.ts b/x-pack/plugins/monitoring/public/lib/format_msg.test.ts new file mode 100644 index 0000000000000..0643e2a6f2b95 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/format_msg.test.ts @@ -0,0 +1,122 @@ +/* + * 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 { formatMsg, formatESMsg } from './format_msg'; + +describe('formatMsg', () => { + test('should prepend the second argument to result', () => { + const actual = formatMsg('error message', 'unit_test'); + + expect(actual).to.equal('unit_test: error message'); + }); + + test('should handle a simple string', () => { + const actual = formatMsg('error message'); + + expect(actual).to.equal('error message'); + }); + + test('should handle a simple Error object', () => { + const err = new Error('error message'); + const actual = formatMsg(err); + + expect(actual).to.equal('error message'); + }); + + test('should handle a simple Angular $http error object', () => { + const err = { + data: { + statusCode: 403, + error: 'Forbidden', + message: + '[security_exception] action [indices:data/read/msearch] is unauthorized for user [user]', + }, + status: 403, + config: {}, + statusText: 'Forbidden', + }; + const actual = formatMsg(err); + + expect(actual).to.equal( + 'Error 403 Forbidden: [security_exception] action [indices:data/read/msearch] is unauthorized for user [user]' + ); + }); + + test('should handle an extended elasticsearch error', () => { + const err = { + resp: { + error: { + root_cause: [ + { + reason: 'I am the detailed message', + }, + ], + }, + }, + }; + + const actual = formatMsg(err); + + expect(actual).to.equal('I am the detailed message'); + }); + + describe('formatESMsg', () => { + test('should return undefined if passed a basic error', () => { + const err = new Error('This is a normal error'); + + const actual = formatESMsg(err); + + expect(actual).to.be(undefined); + }); + + test('should return undefined if passed a string', () => { + const err = 'This is a error string'; + + const actual = formatESMsg(err); + + expect(actual).to.be(undefined); + }); + + test('should return the root_cause if passed an extended elasticsearch', () => { + const err: Record = new Error('This is an elasticsearch error'); + err.resp = { + error: { + root_cause: [ + { + reason: 'I am the detailed message', + }, + ], + }, + }; + + const actual = formatESMsg(err); + + expect(actual).to.equal('I am the detailed message'); + }); + + test('should combine the reason messages if more than one is returned.', () => { + const err: Record = new Error('This is an elasticsearch error'); + err.resp = { + error: { + root_cause: [ + { + reason: 'I am the detailed message 1', + }, + { + reason: 'I am the detailed message 2', + }, + ], + }, + }; + + const actual = formatESMsg(err); + + expect(actual).to.equal('I am the detailed message 1\nI am the detailed message 2'); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/public/lib/format_msg.ts b/x-pack/plugins/monitoring/public/lib/format_msg.ts new file mode 100644 index 0000000000000..97ba63546a9e4 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/format_msg.ts @@ -0,0 +1,84 @@ +/* + * 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 _ from 'lodash'; +import { i18n } from '@kbn/i18n'; +const has = _.has; + +const getRootCause = (err: Record | string) => _.get(err, 'resp.error.root_cause'); + +/** + * Utilize the extended error information returned from elasticsearch + * @param {Error|String} err + * @returns {string} + */ +export const formatESMsg = (err: Record | string) => { + const rootCause = getRootCause(err); + + if (!Array.isArray(rootCause)) { + return; + } + + return rootCause.map((cause: Record) => cause.reason).join('\n'); +}; + +/** + * Formats the error message from an error object, extended elasticsearch + * object or simple string; prepends optional second parameter to the message + * @param {Error|String} err + * @param {String} source - Prefix for message indicating source (optional) + * @returns {string} + */ +export function formatMsg(err: Record | string, source: string = '') { + let message = ''; + if (source) { + message += source + ': '; + } + + const esMsg = formatESMsg(err); + + if (typeof err === 'string') { + message += err; + } else if (esMsg) { + message += esMsg; + } else if (err instanceof Error) { + message += formatMsg.describeError(err); + } else if (has(err, 'status') && has(err, 'data')) { + // is an Angular $http "error object" + if (err.status === -1) { + // status = -1 indicates that the request was failed to reach the server + message += i18n.translate( + 'xpack.monitoring.formatMsg.toaster.unavailableServerErrorMessage', + { + defaultMessage: + 'An HTTP request has failed to connect. ' + + 'Please check if the Kibana server is running and that your browser has a working connection, ' + + 'or contact your system administrator.', + } + ); + } else { + message += i18n.translate('xpack.monitoring.formatMsg.toaster.errorStatusMessage', { + defaultMessage: 'Error {errStatus} {errStatusText}: {errMessage}', + values: { + errStatus: err.status, + errStatusText: err.statusText, + errMessage: err.data.message, + }, + }); + } + } + + return message; +} + +formatMsg.describeError = function (err: Record) { + if (!err) return undefined; + if (err.shortMessage) return err.shortMessage; + if (err.body && err.body.message) return err.body.message; + if (err.message) return err.message; + return '' + err; +}; diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json index 756b8528865ce..79fcff4d840ff 100644 --- a/x-pack/plugins/monitoring/tsconfig.json +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -15,7 +15,6 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_legacy/tsconfig.json" }, { "path": "../../../src/plugins/navigation/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, diff --git a/x-pack/plugins/reporting/common/test/fixtures.ts b/x-pack/plugins/reporting/common/test/fixtures.ts new file mode 100644 index 0000000000000..c7489d54e9504 --- /dev/null +++ b/x-pack/plugins/reporting/common/test/fixtures.ts @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ReportApiJSON } from '../types'; +import type { ReportMock } from './types'; + +const buildMockReport = (baseObj: ReportMock) => ({ + index: '.reporting-2020.04.12', + migration_version: '7.15.0', + browser_type: 'chromium', + max_attempts: 1, + timeout: 300000, + created_by: 'elastic', + kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', + kibana_name: 'spicy.local', + ...baseObj, + payload: { + browserTimezone: 'America/Phoenix', + layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, + version: '7.14.0', + isDeprecated: baseObj.payload.isDeprecated === true, + ...baseObj.payload, + }, +}); + +export const mockJobs: ReportApiJSON[] = [ + buildMockReport({ + id: 'k90e51pk1ieucbae0c3t8wo2', + attempts: 0, + created_at: '2020-04-14T21:01:13.064Z', + jobtype: 'printable_pdf_v2', + meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, + payload: { + spaceId: 'my-space', + objectType: 'canvas workpad', + title: 'My Canvas Workpad', + locatorParams: [ + { + id: 'MY_APP', + }, + ], + } as any, + status: 'pending', + }), + buildMockReport({ + id: 'k90e51pk1ieucbae0c3t8wo1', + attempts: 1, + created_at: '2020-04-14T21:01:13.064Z', + jobtype: 'printable_pdf', + meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, + payload: { + objectType: 'canvas workpad', + title: 'My Canvas Workpad', + }, + started_at: '2020-04-14T21:01:14.526Z', + status: 'processing', + }), + buildMockReport({ + id: 'k90cmthd1gv8cbae0c2le8bo', + attempts: 1, + completed_at: '2020-04-14T20:19:14.748Z', + created_at: '2020-04-14T20:19:02.977Z', + jobtype: 'printable_pdf', + meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, + output: { content_type: 'application/pdf', size: 80262 }, + payload: { + objectType: 'canvas workpad', + title: 'My Canvas Workpad', + }, + started_at: '2020-04-14T20:19:04.073Z', + status: 'completed', + }), + buildMockReport({ + id: 'k906958e1d4wcbae0c9hip1a', + attempts: 1, + completed_at: '2020-04-14T17:21:08.223Z', + created_at: '2020-04-14T17:20:27.326Z', + jobtype: 'printable_pdf', + meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, + output: { + content_type: 'application/pdf', + size: 49468, + warnings: [ + 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded', + ], + }, + payload: { + objectType: 'canvas workpad', + title: 'My Canvas Workpad', + }, + started_at: '2020-04-14T17:20:29.444Z', + status: 'completed_with_warnings', + }), + buildMockReport({ + id: 'k9067y2a1d4wcbae0cad38n0', + attempts: 1, + completed_at: '2020-04-14T17:19:53.244Z', + created_at: '2020-04-14T17:19:31.379Z', + jobtype: 'printable_pdf', + meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, + output: { content_type: 'application/pdf', size: 80262 }, + payload: { + objectType: 'canvas workpad', + title: 'My Canvas Workpad', + }, + started_at: '2020-04-14T17:19:39.883Z', + status: 'completed', + }), + buildMockReport({ + id: 'k9067s1m1d4wcbae0cdnvcms', + attempts: 1, + completed_at: '2020-04-14T17:19:36.822Z', + created_at: '2020-04-14T17:19:23.578Z', + jobtype: 'printable_pdf', + meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, + output: { content_type: 'application/pdf', size: 80262 }, + payload: { + objectType: 'canvas workpad', + title: 'My Canvas Workpad', + }, + started_at: '2020-04-14T17:19:25.247Z', + status: 'completed', + }), + buildMockReport({ + id: 'k9065q3s1d4wcbae0c00fxlh', + attempts: 1, + completed_at: '2020-04-14T17:18:03.910Z', + created_at: '2020-04-14T17:17:47.752Z', + jobtype: 'printable_pdf', + meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, + output: { content_type: 'application/pdf', size: 80262 }, + payload: { + objectType: 'canvas workpad', + title: 'My Canvas Workpad', + }, + started_at: '2020-04-14T17:17:50.379Z', + status: 'completed', + }), + buildMockReport({ + id: 'k905zdw11d34cbae0c3y6tzh', + attempts: 1, + completed_at: '2020-04-14T17:13:03.719Z', + created_at: '2020-04-14T17:12:51.985Z', + jobtype: 'printable_pdf', + meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, + output: { content_type: 'application/pdf', size: 80262 }, + payload: { + objectType: 'canvas workpad', + title: 'My Canvas Workpad', + isDeprecated: true, + }, + started_at: '2020-04-14T17:12:52.431Z', + status: 'completed', + }), + buildMockReport({ + id: 'k8t4ylcb07mi9d006214ifyg', + attempts: 1, + completed_at: '2020-04-09T19:10:10.049Z', + created_at: '2020-04-09T19:09:52.139Z', + jobtype: 'PNG', + meta: { layout: 'png', objectType: 'visualization' }, + output: { content_type: 'image/png', size: 123456789 }, + payload: { + objectType: 'visualization', + title: 'count', + isDeprecated: true, + }, + started_at: '2020-04-09T19:09:54.570Z', + status: 'completed', + }), +]; diff --git a/x-pack/plugins/reporting/common/test/index.ts b/x-pack/plugins/reporting/common/test/index.ts new file mode 100644 index 0000000000000..d9d62a7a877a7 --- /dev/null +++ b/x-pack/plugins/reporting/common/test/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { PayloadMock, ReportMock } from './types'; + +export { mockJobs } from './fixtures'; diff --git a/x-pack/plugins/reporting/common/test/types.ts b/x-pack/plugins/reporting/common/test/types.ts new file mode 100644 index 0000000000000..83ebc1e3358cb --- /dev/null +++ b/x-pack/plugins/reporting/common/test/types.ts @@ -0,0 +1,26 @@ +/* + * 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 { ReportApiJSON } from '../types'; + +/** @internal */ +export interface PayloadMock { + payload: Omit; +} + +/** @internal */ +export type ReportMock = Omit< + ReportApiJSON, + | 'index' + | 'migration_version' + | 'browser_type' + | 'max_attempts' + | 'timeout' + | 'created_by' + | 'payload' +> & + PayloadMock; diff --git a/x-pack/plugins/reporting/public/management/__test__/index.ts b/x-pack/plugins/reporting/public/management/__test__/index.ts new file mode 100644 index 0000000000000..b1dc1b53ec280 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/__test__/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { mockJobs } from '../../../common/test'; + +export { setup } from './report_listing.test.helpers'; + +export type { TestBed, TestDependencies } from './report_listing.test.helpers'; diff --git a/x-pack/plugins/reporting/public/management/__test__/report_listing.test.helpers.tsx b/x-pack/plugins/reporting/public/management/__test__/report_listing.test.helpers.tsx new file mode 100644 index 0000000000000..c7c1b48a04bc8 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/__test__/report_listing.test.helpers.tsx @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { registerTestBed } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import { Observable } from 'rxjs'; +import { UnwrapPromise, SerializableRecord } from '@kbn/utility-types'; + +import type { NotificationsSetup } from '../../../../../../src/core/public'; +import { + applicationServiceMock, + httpServiceMock, + notificationServiceMock, + coreMock, +} from '../../../../../../src/core/public/mocks'; +import type { LocatorPublic, SharePluginSetup } from '../../../../../../src/plugins/share/public'; + +import type { ILicense } from '../../../../licensing/public'; + +import { mockJobs } from '../../../common/test'; + +import { KibanaContextProvider } from '../../shared_imports'; + +import { IlmPolicyStatusContextProvider } from '../../lib/ilm_policy_status_context'; +import { InternalApiClientProvider, ReportingAPIClient } from '../../lib/reporting_api_client'; +import { Job } from '../../lib/job'; + +import { ListingProps as Props, ReportListing } from '../'; + +export interface TestDependencies { + http: ReturnType; + application: ReturnType; + reportingAPIClient: ReportingAPIClient; + license$: Observable; + urlService: SharePluginSetup['url']; + toasts: NotificationsSetup['toasts']; + ilmLocator: LocatorPublic; +} + +const mockPollConfig = { + jobCompletionNotifier: { + interval: 5000, + intervalErrorMultiplier: 3, + }, + jobsRefresh: { + interval: 5000, + intervalErrorMultiplier: 3, + }, +}; + +const validCheck = { + check: () => ({ + state: 'VALID', + message: '', + }), +}; + +const license$ = { + subscribe: (handler: unknown) => { + return (handler as Function)(validCheck); + }, +} as Observable; + +const createTestBed = registerTestBed( + ({ + http, + application, + reportingAPIClient, + license$: l$, + urlService, + toasts, + ...rest + }: Partial & TestDependencies) => ( + + + + + + + + ), + { memoryRouter: { wrapComponent: false } } +); + +export type TestBed = UnwrapPromise>; + +export const setup = async (props?: Partial) => { + const uiSettingsClient = coreMock.createSetup().uiSettings; + const httpService = httpServiceMock.createSetupContract(); + const reportingAPIClient = new ReportingAPIClient(httpService, uiSettingsClient, 'x.x.x'); + + jest + .spyOn(reportingAPIClient, 'list') + .mockImplementation(() => Promise.resolve(mockJobs.map((j) => new Job(j)))); + jest.spyOn(reportingAPIClient, 'total').mockImplementation(() => Promise.resolve(18)); + jest.spyOn(reportingAPIClient, 'migrateReportingIndicesIlmPolicy').mockImplementation(jest.fn()); + + const ilmLocator: LocatorPublic = { + getUrl: jest.fn(), + } as unknown as LocatorPublic; + + const testDependencies: TestDependencies = { + http: httpService, + application: applicationServiceMock.createStartContract(), + toasts: notificationServiceMock.createSetupContract().toasts, + license$, + reportingAPIClient, + ilmLocator, + urlService: { + locators: { + get: () => ilmLocator, + }, + } as unknown as SharePluginSetup['url'], + }; + + const testBed = createTestBed({ ...testDependencies, ...props }); + + const { find, exists, component } = testBed; + + return { + ...testBed, + testDependencies, + actions: { + findListTable: () => find('reportJobListing'), + hasIlmMigrationBanner: () => exists('migrateReportingIndicesPolicyCallOut'), + hasIlmPolicyLink: () => exists('ilmPolicyLink'), + migrateIndices: async () => { + await act(async () => { + find('migrateReportingIndicesButton').simulate('click'); + }); + component.update(); + }, + }, + }; +}; diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.ts b/x-pack/plugins/reporting/public/management/report_listing.test.ts new file mode 100644 index 0000000000000..b6096bfa97a97 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/report_listing.test.ts @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Observable } from 'rxjs'; +import { act } from 'react-dom/test-utils'; + +import type { ILicense } from '../../../licensing/public'; +import type { IlmPolicyMigrationStatus } from '../../common/types'; + +import { ListingProps as Props } from '.'; + +import { setup, TestBed, TestDependencies, mockJobs } from './__test__'; + +describe('ReportListing', () => { + let testBed: TestBed; + + let applicationService: TestDependencies['application']; + + const runSetup = async (props?: Partial) => { + await act(async () => { + testBed = await setup(props); + }); + testBed.component.update(); + }; + + beforeEach(async () => { + await runSetup(); + // Collect all of the injected services so we can mutate for the tests + applicationService = testBed.testDependencies.application; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders a listing with some items', () => { + const { find } = testBed; + expect(find('reportDownloadLink').length).toBe(mockJobs.length); + }); + + it('subscribes to license changes, and unsubscribes on dismount', async () => { + const unsubscribeMock = jest.fn(); + const subMock = { + subscribe: jest.fn().mockReturnValue({ + unsubscribe: unsubscribeMock, + }), + } as unknown as Observable; + + await runSetup({ license$: subMock }); + + expect(subMock.subscribe).toHaveBeenCalled(); + expect(unsubscribeMock).not.toHaveBeenCalled(); + testBed.component.unmount(); + expect(unsubscribeMock).toHaveBeenCalled(); + }); + + it('navigates to a Kibana App in a new tab and is spaces aware', () => { + const { find } = testBed; + + jest.spyOn(window, 'open').mockImplementation(jest.fn()); + jest.spyOn(window, 'focus').mockImplementation(jest.fn()); + + find('euiCollapsedItemActionsButton').first().simulate('click'); + find('reportOpenInKibanaApp').first().simulate('click'); + + expect(window.open).toHaveBeenCalledWith( + '/s/my-space/app/reportingRedirect?jobId=k90e51pk1ieucbae0c3t8wo2', + '_blank' + ); + }); + + describe('ILM policy', () => { + let httpService: TestDependencies['http']; + let urlService: TestDependencies['urlService']; + let toasts: TestDependencies['toasts']; + let reportingAPIClient: TestDependencies['reportingAPIClient']; + + /** + * Simulate a fresh page load, useful for network requests and other effects + * that happen only at first load. + */ + const remountComponent = async () => { + const { component } = testBed; + act(() => { + component.unmount(); + }); + await act(async () => { + component.mount(); + }); + // Flush promises + await new Promise((r) => setImmediate(r)); + component.update(); + }; + + beforeEach(async () => { + await runSetup(); + // Collect all of the injected services so we can mutate for the tests + applicationService = testBed.testDependencies.application; + applicationService.capabilities = { + catalogue: {}, + navLinks: {}, + management: { data: { index_lifecycle_management: true } }, + }; + httpService = testBed.testDependencies.http; + urlService = testBed.testDependencies.urlService; + toasts = testBed.testDependencies.toasts; + reportingAPIClient = testBed.testDependencies.reportingAPIClient; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('shows the migrate banner when migration status is not "OK"', async () => { + const { actions } = testBed; + const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + httpService.get.mockResolvedValue({ status }); + await remountComponent(); + expect(actions.hasIlmMigrationBanner()).toBe(true); + }); + + it('does not show the migrate banner when migration status is "OK"', async () => { + const { actions } = testBed; + const status: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValue({ status }); + await remountComponent(); + expect(actions.hasIlmMigrationBanner()).toBe(false); + }); + + it('hides the ILM policy link if there is no ILM policy', async () => { + const { actions } = testBed; + const status: IlmPolicyMigrationStatus = 'policy-not-found'; + httpService.get.mockResolvedValue({ status }); + await remountComponent(); + expect(actions.hasIlmPolicyLink()).toBe(false); + }); + + it('hides the ILM policy link if there is no ILM policy locator', async () => { + const { actions } = testBed; + jest.spyOn(urlService.locators, 'get').mockReturnValue(undefined); + const status: IlmPolicyMigrationStatus = 'ok'; // should never happen, but need to test that when the locator is missing we don't render the link + httpService.get.mockResolvedValue({ status }); + await remountComponent(); + expect(actions.hasIlmPolicyLink()).toBe(false); + }); + + it('always shows the ILM policy link if there is an ILM policy', async () => { + const { actions } = testBed; + const status: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValue({ status }); + await remountComponent(); + expect(actions.hasIlmPolicyLink()).toBe(true); + + const status2: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + httpService.get.mockResolvedValue({ status: status2 }); + await remountComponent(); + expect(actions.hasIlmPolicyLink()).toBe(true); + }); + + it('hides the banner after migrating indices', async () => { + const { actions } = testBed; + const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + const status2: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValueOnce({ status }); + httpService.get.mockResolvedValueOnce({ status: status2 }); + await remountComponent(); + + expect(actions.hasIlmMigrationBanner()).toBe(true); + await actions.migrateIndices(); + expect(actions.hasIlmMigrationBanner()).toBe(false); + expect(actions.hasIlmPolicyLink()).toBe(true); + expect(toasts.addSuccess).toHaveBeenCalledTimes(1); + }); + + it('informs users when migrations failed', async () => { + const { actions } = testBed; + const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + httpService.get.mockResolvedValueOnce({ status }); + (reportingAPIClient.migrateReportingIndicesIlmPolicy as jest.Mock).mockRejectedValueOnce( + new Error('oops!') + ); + await remountComponent(); + + expect(actions.hasIlmMigrationBanner()).toBe(true); + await actions.migrateIndices(); + expect(toasts.addError).toHaveBeenCalledTimes(1); + expect(actions.hasIlmMigrationBanner()).toBe(true); + expect(actions.hasIlmPolicyLink()).toBe(true); + }); + + it('only shows the link to the ILM policy if UI capabilities allow it', async () => { + applicationService.capabilities = { + catalogue: {}, + navLinks: {}, + management: { data: { index_lifecycle_management: false } }, + }; + await remountComponent(); + + expect(testBed.actions.hasIlmPolicyLink()).toBe(false); + + applicationService.capabilities = { + catalogue: {}, + navLinks: {}, + management: { data: { index_lifecycle_management: true } }, + }; + + await remountComponent(); + + expect(testBed.actions.hasIlmPolicyLink()).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx deleted file mode 100644 index 577d64be38a54..0000000000000 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ /dev/null @@ -1,479 +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 { registerTestBed } from '@kbn/test/jest'; -import type { SerializableRecord, UnwrapPromise } from '@kbn/utility-types'; -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import type { Observable } from 'rxjs'; -import type { IUiSettingsClient } from 'src/core/public'; -import { ListingProps as Props, ReportListing } from '.'; -import type { NotificationsSetup } from '../../../../../src/core/public'; -import { - applicationServiceMock, - httpServiceMock, - notificationServiceMock, - coreMock, -} from '../../../../../src/core/public/mocks'; -import type { LocatorPublic, SharePluginSetup } from '../../../../../src/plugins/share/public'; -import type { ILicense } from '../../../licensing/public'; -import type { IlmPolicyMigrationStatus, ReportApiJSON } from '../../common/types'; -import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; -import { Job } from '../lib/job'; -import { InternalApiClientProvider, ReportingAPIClient } from '../lib/reporting_api_client'; -import { KibanaContextProvider } from '../shared_imports'; - -interface PayloadMock { - payload: Omit; -} -type ReportMock = Omit< - ReportApiJSON, - | 'index' - | 'migration_version' - | 'browser_type' - | 'max_attempts' - | 'timeout' - | 'created_by' - | 'payload' -> & - PayloadMock; - -const buildMockReport = (baseObj: ReportMock) => ({ - index: '.reporting-2020.04.12', - migration_version: '7.15.0', - browser_type: 'chromium', - max_attempts: 1, - timeout: 300000, - created_by: 'elastic', - kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', - kibana_name: 'spicy.local', - ...baseObj, - payload: { - browserTimezone: 'America/Phoenix', - layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, - version: '7.14.0', - isDeprecated: baseObj.payload.isDeprecated === true, - ...baseObj.payload, - }, -}); - -const mockJobs: ReportApiJSON[] = [ - buildMockReport({ - id: 'k90e51pk1ieucbae0c3t8wo2', - attempts: 0, - created_at: '2020-04-14T21:01:13.064Z', - jobtype: 'printable_pdf_v2', - meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, - payload: { - spaceId: 'my-space', - objectType: 'canvas workpad', - title: 'My Canvas Workpad', - locatorParams: [ - { - id: 'MY_APP', - }, - ], - } as any, - status: 'pending', - }), - buildMockReport({ - id: 'k90e51pk1ieucbae0c3t8wo1', - attempts: 1, - created_at: '2020-04-14T21:01:13.064Z', - jobtype: 'printable_pdf', - meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, - payload: { - objectType: 'canvas workpad', - title: 'My Canvas Workpad', - }, - started_at: '2020-04-14T21:01:14.526Z', - status: 'processing', - }), - buildMockReport({ - id: 'k90cmthd1gv8cbae0c2le8bo', - attempts: 1, - completed_at: '2020-04-14T20:19:14.748Z', - created_at: '2020-04-14T20:19:02.977Z', - jobtype: 'printable_pdf', - meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, - output: { content_type: 'application/pdf', size: 80262 }, - payload: { - objectType: 'canvas workpad', - title: 'My Canvas Workpad', - }, - started_at: '2020-04-14T20:19:04.073Z', - status: 'completed', - }), - buildMockReport({ - id: 'k906958e1d4wcbae0c9hip1a', - attempts: 1, - completed_at: '2020-04-14T17:21:08.223Z', - created_at: '2020-04-14T17:20:27.326Z', - jobtype: 'printable_pdf', - meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, - output: { - content_type: 'application/pdf', - size: 49468, - warnings: [ - 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded', - ], - }, - payload: { - objectType: 'canvas workpad', - title: 'My Canvas Workpad', - }, - started_at: '2020-04-14T17:20:29.444Z', - status: 'completed_with_warnings', - }), - buildMockReport({ - id: 'k9067y2a1d4wcbae0cad38n0', - attempts: 1, - completed_at: '2020-04-14T17:19:53.244Z', - created_at: '2020-04-14T17:19:31.379Z', - jobtype: 'printable_pdf', - meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, - output: { content_type: 'application/pdf', size: 80262 }, - payload: { - objectType: 'canvas workpad', - title: 'My Canvas Workpad', - }, - started_at: '2020-04-14T17:19:39.883Z', - status: 'completed', - }), - buildMockReport({ - id: 'k9067s1m1d4wcbae0cdnvcms', - attempts: 1, - completed_at: '2020-04-14T17:19:36.822Z', - created_at: '2020-04-14T17:19:23.578Z', - jobtype: 'printable_pdf', - meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, - output: { content_type: 'application/pdf', size: 80262 }, - payload: { - objectType: 'canvas workpad', - title: 'My Canvas Workpad', - }, - started_at: '2020-04-14T17:19:25.247Z', - status: 'completed', - }), - buildMockReport({ - id: 'k9065q3s1d4wcbae0c00fxlh', - attempts: 1, - completed_at: '2020-04-14T17:18:03.910Z', - created_at: '2020-04-14T17:17:47.752Z', - jobtype: 'printable_pdf', - meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, - output: { content_type: 'application/pdf', size: 80262 }, - payload: { - objectType: 'canvas workpad', - title: 'My Canvas Workpad', - }, - started_at: '2020-04-14T17:17:50.379Z', - status: 'completed', - }), - buildMockReport({ - id: 'k905zdw11d34cbae0c3y6tzh', - attempts: 1, - completed_at: '2020-04-14T17:13:03.719Z', - created_at: '2020-04-14T17:12:51.985Z', - jobtype: 'printable_pdf', - meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, - output: { content_type: 'application/pdf', size: 80262 }, - payload: { - objectType: 'canvas workpad', - title: 'My Canvas Workpad', - isDeprecated: true, - }, - started_at: '2020-04-14T17:12:52.431Z', - status: 'completed', - }), - buildMockReport({ - id: 'k8t4ylcb07mi9d006214ifyg', - attempts: 1, - completed_at: '2020-04-09T19:10:10.049Z', - created_at: '2020-04-09T19:09:52.139Z', - jobtype: 'PNG', - meta: { layout: 'png', objectType: 'visualization' }, - output: { content_type: 'image/png', size: 123456789 }, - payload: { - objectType: 'visualization', - title: 'count', - isDeprecated: true, - }, - started_at: '2020-04-09T19:09:54.570Z', - status: 'completed', - }), -]; - -const validCheck = { - check: () => ({ - state: 'VALID', - message: '', - }), -}; - -const license$ = { - subscribe: (handler: unknown) => { - return (handler as Function)(validCheck); - }, -} as Observable; - -const mockPollConfig = { - jobCompletionNotifier: { - interval: 5000, - intervalErrorMultiplier: 3, - }, - jobsRefresh: { - interval: 5000, - intervalErrorMultiplier: 3, - }, -}; - -describe('ReportListing', () => { - let httpService: ReturnType; - let uiSettingsClient: IUiSettingsClient; - let applicationService: ReturnType; - let ilmLocator: undefined | LocatorPublic; - let urlService: SharePluginSetup['url']; - let testBed: UnwrapPromise>; - let toasts: NotificationsSetup['toasts']; - let reportingAPIClient: ReportingAPIClient; - - const createTestBed = registerTestBed( - (props?: Partial) => ( - - - - - - - - ), - { memoryRouter: { wrapComponent: false } } - ); - - const setup = async (props?: Partial) => { - const tb = await createTestBed(props); - const { find, exists, component } = tb; - - return { - ...tb, - actions: { - findListTable: () => find('reportJobListing'), - hasIlmMigrationBanner: () => exists('migrateReportingIndicesPolicyCallOut'), - hasIlmPolicyLink: () => exists('ilmPolicyLink'), - migrateIndices: async () => { - await act(async () => { - find('migrateReportingIndicesButton').simulate('click'); - }); - component.update(); - }, - }, - }; - }; - - const runSetup = async (props?: Partial) => { - await act(async () => { - testBed = await setup(props); - }); - testBed.component.update(); - }; - - beforeEach(async () => { - toasts = notificationServiceMock.createSetupContract().toasts; - httpService = httpServiceMock.createSetupContract(); - uiSettingsClient = coreMock.createSetup().uiSettings; - applicationService = applicationServiceMock.createStartContract(); - applicationService.capabilities = { - catalogue: {}, - navLinks: {}, - management: { data: { index_lifecycle_management: true } }, - }; - ilmLocator = { - getUrl: jest.fn(), - } as unknown as LocatorPublic; - - reportingAPIClient = new ReportingAPIClient(httpService, uiSettingsClient, 'x.x.x'); - - jest - .spyOn(reportingAPIClient, 'list') - .mockImplementation(() => Promise.resolve(mockJobs.map((j) => new Job(j)))); - jest.spyOn(reportingAPIClient, 'total').mockImplementation(() => Promise.resolve(18)); - jest - .spyOn(reportingAPIClient, 'migrateReportingIndicesIlmPolicy') - .mockImplementation(jest.fn()); - - urlService = { - locators: { - get: () => ilmLocator, - }, - } as unknown as SharePluginSetup['url']; - await runSetup(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('renders a listing with some items', () => { - const { find } = testBed; - expect(find('reportDownloadLink').length).toBe(mockJobs.length); - }); - - it('subscribes to license changes, and unsubscribes on dismount', async () => { - const unsubscribeMock = jest.fn(); - const subMock = { - subscribe: jest.fn().mockReturnValue({ - unsubscribe: unsubscribeMock, - }), - } as unknown as Observable; - - await runSetup({ license$: subMock }); - - expect(subMock.subscribe).toHaveBeenCalled(); - expect(unsubscribeMock).not.toHaveBeenCalled(); - testBed.component.unmount(); - expect(unsubscribeMock).toHaveBeenCalled(); - }); - - it('navigates to a Kibana App in a new tab and is spaces aware', () => { - const { find } = testBed; - - jest.spyOn(window, 'open').mockImplementation(jest.fn()); - jest.spyOn(window, 'focus').mockImplementation(jest.fn()); - - find('euiCollapsedItemActionsButton').first().simulate('click'); - find('reportOpenInKibanaApp').first().simulate('click'); - - expect(window.open).toHaveBeenCalledWith( - '/s/my-space/app/reportingRedirect?jobId=k90e51pk1ieucbae0c3t8wo2', - '_blank' - ); - }); - - describe('ILM policy', () => { - beforeEach(async () => { - httpService = httpServiceMock.createSetupContract(); - ilmLocator = { - getUrl: jest.fn(), - } as unknown as LocatorPublic; - - urlService = { - locators: { - get: () => ilmLocator, - }, - } as unknown as SharePluginSetup['url']; - - await runSetup(); - }); - - it('shows the migrate banner when migration status is not "OK"', async () => { - const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; - httpService.get.mockResolvedValue({ status }); - await runSetup(); - const { actions } = testBed; - expect(actions.hasIlmMigrationBanner()).toBe(true); - }); - - it('does not show the migrate banner when migration status is "OK"', async () => { - const status: IlmPolicyMigrationStatus = 'ok'; - httpService.get.mockResolvedValue({ status }); - await runSetup(); - const { actions } = testBed; - expect(actions.hasIlmMigrationBanner()).toBe(false); - }); - - it('hides the ILM policy link if there is no ILM policy', async () => { - const status: IlmPolicyMigrationStatus = 'policy-not-found'; - httpService.get.mockResolvedValue({ status }); - await runSetup(); - const { actions } = testBed; - expect(actions.hasIlmPolicyLink()).toBe(false); - }); - - it('hides the ILM policy link if there is no ILM policy locator', async () => { - ilmLocator = undefined; - const status: IlmPolicyMigrationStatus = 'ok'; // should never happen, but need to test that when the locator is missing we don't render the link - httpService.get.mockResolvedValue({ status }); - await runSetup(); - const { actions } = testBed; - expect(actions.hasIlmPolicyLink()).toBe(false); - }); - - it('always shows the ILM policy link if there is an ILM policy', async () => { - const status: IlmPolicyMigrationStatus = 'ok'; - httpService.get.mockResolvedValue({ status }); - await runSetup(); - const { actions } = testBed; - expect(actions.hasIlmPolicyLink()).toBe(true); - - const status2: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; - httpService.get.mockResolvedValue({ status: status2 }); - await runSetup(); - expect(actions.hasIlmPolicyLink()).toBe(true); - }); - - it('hides the banner after migrating indices', async () => { - const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; - const status2: IlmPolicyMigrationStatus = 'ok'; - httpService.get.mockResolvedValueOnce({ status }); - httpService.get.mockResolvedValueOnce({ status: status2 }); - await runSetup(); - const { actions } = testBed; - - expect(actions.hasIlmMigrationBanner()).toBe(true); - await actions.migrateIndices(); - expect(actions.hasIlmMigrationBanner()).toBe(false); - expect(actions.hasIlmPolicyLink()).toBe(true); - expect(toasts.addSuccess).toHaveBeenCalledTimes(1); - }); - - it('informs users when migrations failed', async () => { - const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; - httpService.get.mockResolvedValueOnce({ status }); - (reportingAPIClient.migrateReportingIndicesIlmPolicy as jest.Mock).mockRejectedValueOnce( - new Error('oops!') - ); - await runSetup(); - const { actions } = testBed; - - expect(actions.hasIlmMigrationBanner()).toBe(true); - await actions.migrateIndices(); - expect(toasts.addError).toHaveBeenCalledTimes(1); - expect(actions.hasIlmMigrationBanner()).toBe(true); - expect(actions.hasIlmPolicyLink()).toBe(true); - }); - - it('only shows the link to the ILM policy if UI capabilities allow it', async () => { - applicationService.capabilities = { - catalogue: {}, - navLinks: {}, - management: { data: { index_lifecycle_management: false } }, - }; - await runSetup(); - - expect(testBed.actions.hasIlmPolicyLink()).toBe(false); - - applicationService.capabilities = { - catalogue: {}, - navLinks: {}, - management: { data: { index_lifecycle_management: true } }, - }; - - await runSetup(); - - expect(testBed.actions.hasIlmPolicyLink()).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/reporting/server/deprecations/migrage_existing_indices_ilm_policy.test.ts b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/deprecations/migrage_existing_indices_ilm_policy.test.ts rename to x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.test.ts diff --git a/x-pack/plugins/runtime_fields/tsconfig.json b/x-pack/plugins/runtime_fields/tsconfig.json index 5dc704ec57693..321854e2d7bbe 100644 --- a/x-pack/plugins/runtime_fields/tsconfig.json +++ b/x-pack/plugins/runtime_fields/tsconfig.json @@ -13,6 +13,5 @@ "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_legacy/tsconfig.json"} ] } diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 7d4badcd3507c..8c6c3fdd961a8 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isEmpty } from 'lodash'; +import { isEmpty, capitalize } from 'lodash'; import type { EntriesArray, @@ -73,3 +73,11 @@ export const getRuleStatusText = ( : value != null ? value : null; + +export const getCapitalizedRuleStatusText = ( + value: RuleExecutionStatus | null | undefined +): string | null => { + const status = getRuleStatusText(value); + + return status != null ? capitalize(status) : null; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.test.tsx new file mode 100644 index 0000000000000..71a18ee2cfef9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.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 React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { HealthTruncateText } from '.'; + +describe('Component HealthTruncateText', () => { + it('should render component without errors', () => { + render({'Test'}); + + expect(screen.getByTestId('testItem')).toHaveTextContent('Test'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.tsx b/x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.tsx new file mode 100644 index 0000000000000..0344104889832 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiHealth, EuiToolTip, EuiHealthProps } from '@elastic/eui'; +import styled from 'styled-components'; + +const StatusTextWrapper = styled.div` + width: 100%; + display: inline-grid; +`; + +interface HealthTruncateTextProps { + healthColor?: EuiHealthProps['color']; + tooltipContent?: React.ReactNode; + dataTestSubj?: string; +} + +/** + * Allows text in EuiHealth to be properly truncated with tooltip + * @param healthColor - color for EuiHealth component + * @param tooltipContent - tooltip content + */ +export const HealthTruncateText: React.FC = ({ + tooltipContent, + children, + healthColor, + dataTestSubj, +}) => ( + + + + {children} + + + +); + +HealthTruncateText.displayName = 'HealthTruncateText'; diff --git a/x-pack/plugins/security_solution/public/common/components/popover_items/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/popover_items/index.test.tsx new file mode 100644 index 0000000000000..5ffbbfc0ac08c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/popover_items/index.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { PopoverItems, PopoverItemsProps } from '.'; +import { TestProviders } from '../../mock'; +import { render, screen } from '@testing-library/react'; +import { within } from '@testing-library/dom'; + +const mockTags = ['Elastic', 'Endpoint', 'Data Protection', 'ML', 'Continuous Monitoring']; + +const renderHelper = (props: Partial> = {}) => + render( + + {item}} + {...props} + /> + + ); + +const getButton = () => screen.getByRole('button', { name: 'show mocks' }); +const withinPopover = () => within(screen.getByTestId('tagsDisplayPopoverWrapper')); + +describe('Component PopoverItems', () => { + it('shoud render only 2 first items in display and rest in popup', async () => { + renderHelper({ numberOfItemsToDisplay: 2 }); + mockTags.slice(0, 2).forEach((tag) => { + expect(screen.getByText(tag)).toBeInTheDocument(); + }); + + // items not rendered yet + mockTags.slice(2).forEach((tag) => { + expect(screen.queryByText(tag)).toBeNull(); + }); + + getButton().click(); + expect(await screen.findByTestId('tagsDisplayPopoverWrapper')).toBeInTheDocument(); + + // items rendered in popup + mockTags.slice(2).forEach((tag) => { + expect(withinPopover().getByText(tag)).toBeInTheDocument(); + }); + }); + + it('shoud render popover button and items in popover without popover title', () => { + renderHelper(); + mockTags.forEach((tag) => { + expect(screen.queryByText(tag)).toBeNull(); + }); + getButton().click(); + + mockTags.forEach((tag) => { + expect(withinPopover().queryByText(tag)).toBeInTheDocument(); + }); + + expect(screen.queryByTestId('tagsDisplayPopoverTitle')).toBeNull(); + }); + + it('shoud render popover title', async () => { + renderHelper({ popoverTitle: 'Tags popover title' }); + + getButton().click(); + + expect(await screen.findByTestId('tagsDisplayPopoverWrapper')).toBeInTheDocument(); + expect(screen.getByTestId('tagsDisplayPopoverTitle')).toHaveTextContent('Tags popover title'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/popover_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/popover_items/index.tsx new file mode 100644 index 0000000000000..d0c806e7cae98 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/popover_items/index.tsx @@ -0,0 +1,116 @@ +/* + * 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 { + EuiPopover, + EuiBadgeGroup, + EuiBadge, + EuiPopoverTitle, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import styled from 'styled-components'; + +export interface PopoverItemsProps { + renderItem: (item: T, index: number, items: T[]) => React.ReactNode; + items: T[]; + popoverButtonTitle: string; + popoverButtonIcon?: string; + popoverTitle?: string; + numberOfItemsToDisplay?: number; + dataTestPrefix?: string; +} + +interface OverflowListProps { + readonly items: T[]; +} + +const PopoverItemsWrapper = styled(EuiFlexGroup)` + width: 100%; +`; + +const PopoverWrapper = styled(EuiBadgeGroup)` + max-height: 200px; + max-width: 600px; + overflow: auto; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; +`; + +/** + * Component to render list of items in popover, wicth configurabe number of display items by default + * @param items - array of items to render + * @param renderItem - render function that render item, arguments: item, index, items[] + * @param popoverTitle - title of popover + * @param popoverButtonTitle - title of popover button that triggers popover + * @param popoverButtonIcon - icon of popover button that triggers popover + * @param numberOfItemsToDisplay - number of items to render that are no in popover, defaults to 0 + * @param dataTestPrefix - data-test-subj prefix to apply to elements + */ +const PopoverItemsComponent = ({ + items, + renderItem, + popoverTitle, + popoverButtonTitle, + popoverButtonIcon, + numberOfItemsToDisplay = 0, + dataTestPrefix = 'items', +}: PopoverItemsProps) => { + const [isExceptionOverflowPopoverOpen, setIsExceptionOverflowPopoverOpen] = useState(false); + + const OverflowList = ({ items: itemsToRender }: OverflowListProps) => ( + <>{itemsToRender.map(renderItem)} + ); + + if (items.length <= numberOfItemsToDisplay) { + return ( + + + + ); + } + + return ( + + + + + setIsExceptionOverflowPopoverOpen(!isExceptionOverflowPopoverOpen)} + onClickAriaLabel={popoverButtonTitle} + > + {popoverButtonTitle} + + } + isOpen={isExceptionOverflowPopoverOpen} + closePopover={() => setIsExceptionOverflowPopoverOpen(!isExceptionOverflowPopoverOpen)} + repositionOnScroll + > + {popoverTitle ? ( + + {popoverTitle} + + ) : null} + + + + + + ); +}; + +const MemoizedPopoverItems = React.memo(PopoverItemsComponent); +MemoizedPopoverItems.displayName = 'PopoverItems'; + +export const PopoverItems = MemoizedPopoverItems as typeof PopoverItemsComponent; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.test.tsx new file mode 100644 index 0000000000000..d57bfcb329a77 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.test.tsx @@ -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 React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { RuleExecutionStatusBadge } from '.'; + +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; + +describe('Component RuleExecutionStatus', () => { + it('should render component correctly with capitalized status text', () => { + render(); + + expect(screen.getByText('Succeeded')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.tsx new file mode 100644 index 0000000000000..9203480716e48 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status_badge/index.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { getEmptyTagValue } from '../../../../common/components/empty_value'; +import { HealthTruncateText } from '../../../../common/components/health_truncate_text'; +import { getStatusColor } from '../rule_status/helpers'; + +import { getCapitalizedRuleStatusText } from '../../../../../common/detection_engine/utils'; +import type { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; + +interface RuleExecutionStatusBadgeProps { + status: RuleExecutionStatus | null | undefined; +} + +/** + * Shows rule execution status + * @param status - rule execution status + */ +const RuleExecutionStatusBadgeComponent = ({ status }: RuleExecutionStatusBadgeProps) => { + const displayStatus = getCapitalizedRuleStatusText(status); + return ( + + {displayStatus ?? getEmptyTagValue()} + + ); +}; + +export const RuleExecutionStatusBadge = React.memo(RuleExecutionStatusBadgeComponent); + +RuleExecutionStatusBadge.displayName = 'RuleExecutionStatusBadge'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.test.tsx index 611ea3e443124..cff7b50a5ea28 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.test.tsx @@ -14,6 +14,6 @@ describe('SeverityBadge', () => { it('renders correctly', () => { const wrapper = shallow(); - expect(wrapper.find('EuiHealth')).toHaveLength(1); + expect(wrapper.find('HealthTruncateText')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx index 728e0ec871e93..1e1a5f1c35bc4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx @@ -7,28 +7,28 @@ import { upperFirst } from 'lodash/fp'; import React from 'react'; -import { EuiHealth } from '@elastic/eui'; -import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { HealthTruncateText } from '../../../../common/components/health_truncate_text'; interface Props { value: string; } -const SeverityBadgeComponent: React.FC = ({ value }) => ( - - {upperFirst(value)} - -); +const SeverityBadgeComponent: React.FC = ({ value }) => { + const displayValue = upperFirst(value); + const color = 'low' + ? euiLightVars.euiColorVis0 + : value === 'medium' + ? euiLightVars.euiColorVis5 + : value === 'high' + ? euiLightVars.euiColorVis7 + : euiLightVars.euiColorVis9; + + return ( + + {displayValue} + + ); +}; export const SeverityBadge = React.memo(SeverityBadgeComponent); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index c00a96b414778..225915fa312c1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -9,10 +9,10 @@ import { EuiBasicTableColumn, EuiTableActionsColumnType, EuiText, - EuiHealth, EuiToolTip, EuiIcon, EuiLink, + EuiBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import * as H from 'history'; @@ -25,9 +25,10 @@ import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../../../common/components/formatted_date'; import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { ActionToaster } from '../../../../../common/components/toasters'; -import { getStatusColor } from '../../../../components/rules/rule_status/helpers'; +import { PopoverItems } from '../../../../../common/components/popover_items'; import { RuleSwitch } from '../../../../components/rules/rule_switch'; import { SeverityBadge } from '../../../../components/rules/severity_badge'; +import { RuleExecutionStatusBadge } from '../../../../components/rules/rule_execution_status_badge'; import * as i18n from '../translations'; import { deleteRulesAction, @@ -39,8 +40,7 @@ import { RulesTableAction } from '../../../../containers/detection_engine/rules/ import { LinkAnchor } from '../../../../../common/components/links'; import { getToolTipContent, canEditRuleWithActions } from '../../../../../common/utils/privileges'; import { PopoverTooltip } from './popover_tooltip'; -import { TagsDisplay } from './tag_display'; -import { getRuleStatusText } from '../../../../../../common/detection_engine/utils'; + import { APP_UI_ID, SecurityPageName, @@ -163,22 +163,53 @@ export const getColumns = ({ field: 'name', name: i18n.COLUMN_RULE, render: (value: Rule['name'], item: Rule) => ( - void }) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(item.id), - }); - }} - href={formatUrl(getRuleDetailsUrl(item.id))} - > - {value} - + + void }) => { + ev.preventDefault(); + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(item.id), + }); + }} + href={formatUrl(getRuleDetailsUrl(item.id))} + > + {value} + + ), - width: '20%', + width: '38%', sortable: true, + truncateText: true, + }, + { + field: 'tags', + name: null, + align: 'center', + render: (tags: Rule['tags']) => { + if (tags.length === 0) { + return getEmptyTagValue(); + } + + const renderItem = (tag: string, i: number) => ( + + {tag} + + ); + return ( + + ); + }, + width: '65px', + truncateText: true, }, { field: 'risk_score', @@ -188,13 +219,15 @@ export const getColumns = ({ {value} ), - width: '10%', + width: '85px', + truncateText: true, }, { field: 'severity', name: i18n.COLUMN_SEVERITY, render: (value: Rule['severity']) => , width: '12%', + truncateText: true, }, { field: 'status_date', @@ -211,21 +244,15 @@ export const getColumns = ({ /> ); }, - width: '14%', + width: '16%', + truncateText: true, }, { field: 'status', name: i18n.COLUMN_LAST_RESPONSE, - render: (value: Rule['status']) => { - return ( - <> - - {getRuleStatusText(value) ?? getEmptyTagValue()} - - - ); - }, - width: '12%', + render: (value: Rule['status']) => , + width: '16%', + truncateText: true, }, { field: 'updated_at', @@ -243,7 +270,8 @@ export const getColumns = ({ ); }, sortable: true, - width: '14%', + width: '18%', + truncateText: true, }, { field: 'version', @@ -257,18 +285,8 @@ export const getColumns = ({ ); }, - width: '8%', - }, - { - field: 'tags', - name: i18n.COLUMN_TAGS, - render: (value: Rule['tags']) => { - if (value.length > 0) { - return ; - } - return getEmptyTagValue(); - }, - width: '20%', + width: '65px', + truncateText: true, }, { align: 'center', @@ -295,6 +313,7 @@ export const getColumns = ({ ), sortable: true, width: '95px', + truncateText: true, }, ]; const actions: RulesColumns[] = [ @@ -326,34 +345,33 @@ export const getMonitoringColumns = ( name: i18n.COLUMN_RULE, render: (value: RuleStatus['current_status']['status'], item: RuleStatusRowItemType) => { return ( - void }) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(item.id), - }); - }} - href={formatUrl(getRuleDetailsUrl(item.id))} - > - {/* Temporary fix if on upgrade a rule has a status of 'partial failure' we want to display that text as 'warning' */} - {/* On the next subsequent rule run, that 'partial failure' status will be re-written as a 'warning' status */} - {/* and this code will no longer be necessary */} - {/* TODO: remove this code in 8.0.0 */} - {value === 'partial failure' ? 'warning' : value} - + + void }) => { + ev.preventDefault(); + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(item.id), + }); + }} + href={formatUrl(getRuleDetailsUrl(item.id))} + > + {value} + + ); }, - width: '24%', + width: '28%', + truncateText: true, }, { field: 'current_status.bulk_create_time_durations', name: ( <> - {i18n.COLUMN_INDEXING_TIMES}{' '} + {i18n.COLUMN_INDEXING_TIMES} - + ), @@ -363,14 +381,15 @@ export const getMonitoringColumns = ( ), width: '14%', + truncateText: true, }, { field: 'current_status.search_after_time_durations', name: ( <> - {i18n.COLUMN_QUERY_TIMES}{' '} + {i18n.COLUMN_QUERY_TIMES} - + ), @@ -380,6 +399,7 @@ export const getMonitoringColumns = ( ), width: '14%', + truncateText: true, }, { field: 'current_status.gap', @@ -411,6 +431,16 @@ export const getMonitoringColumns = ( ), width: '14%', + truncateText: true, + }, + { + field: 'current_status.status', + name: i18n.COLUMN_LAST_RESPONSE, + render: (value: RuleStatus['current_status']['status']) => ( + + ), + width: '12%', + truncateText: true, }, { field: 'current_status.status_date', @@ -427,21 +457,8 @@ export const getMonitoringColumns = ( /> ); }, - width: '20%', - }, - { - field: 'current_status.status', - name: i18n.COLUMN_LAST_RESPONSE, - render: (value: RuleStatus['current_status']['status']) => { - return ( - <> - - {getRuleStatusText(value) ?? getEmptyTagValue()} - - - ); - }, - width: '16%', + width: '18%', + truncateText: true, }, { field: 'activate', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index 3ba5db820544f..17eafecbae34f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -11,15 +11,18 @@ import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui'; import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { DEFAULT_RELATIVE_DATE_THRESHOLD } from '../../../../../../../common/constants'; import { FormatUrl } from '../../../../../../common/components/link_to'; +import { PopoverItems } from '../../../../../../common/components/popover_items'; import { FormattedRelativePreferenceDate } from '../../../../../../common/components/formatted_date'; - +import { getRuleDetailsUrl } from '../../../../../../common/components/link_to/redirect_to_detection_engine'; +import { LinkAnchor } from '../../../../../../common/components/links'; import * as i18n from './translations'; import { ExceptionListInfo } from './use_all_exception_lists'; -import { ExceptionOverflowDisplay } from './exceptions_overflow_display'; import { ExceptionsTableItem } from './types'; export type AllExceptionListsColumns = EuiBasicTableColumn; +const RULES_TO_DISPLAY = 1; + export const getAllExceptionListsColumns = ( onExport: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void, onDelete: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void, @@ -32,10 +35,10 @@ export const getAllExceptionListsColumns = ( name: i18n.EXCEPTION_LIST_ID_TITLE, truncateText: true, dataType: 'string', - width: '15%', + width: '20%', render: (value: ExceptionListInfo['list_id']) => ( - -

{value}

+ + {value} ), }, @@ -45,37 +48,57 @@ export const getAllExceptionListsColumns = ( name: i18n.EXCEPTION_LIST_NAME, truncateText: true, dataType: 'string', - width: '10%', + width: '20%', render: (value: ExceptionListInfo['name']) => ( - -

{value}

+ + {value} ), }, { - align: 'center', - field: 'rules', - name: i18n.NUMBER_RULES_ASSIGNED_TO_TITLE, - truncateText: true, - dataType: 'number', - width: '10%', - render: (value: ExceptionListInfo['rules']) => { - return

{value.length}

; - }, - }, - { - align: 'left', field: 'rules', name: i18n.RULES_ASSIGNED_TO_TITLE, - truncateText: true, dataType: 'string', - width: '20%', - render: (value: ExceptionListInfo['rules']) => { + width: '30%', + render: (rules: ExceptionListInfo['rules']) => { + const renderItem = ( + { id, name }: T, + index: number, + items: T[] + ) => { + const ruleHref = formatUrl(getRuleDetailsUrl(id)); + const isSeparator = index !== items.length - 1; + return ( + <> + + <> + void }) => { + ev.preventDefault(); + navigateToUrl(ruleHref); + }} + href={ruleHref} + > + {name} + {isSeparator && ','} + + + + {isSeparator && ' '} + + ); + }; + return ( - ); }, @@ -86,7 +109,7 @@ export const getAllExceptionListsColumns = ( name: i18n.LIST_DATE_CREATED_TITLE, truncateText: true, dataType: 'date', - width: '14%', + width: '15%', render: (value: ExceptionListInfo['created_at']) => ( ( ( - - ), - }, - { - align: 'center', - width: '25px', - isExpander: false, - render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => ( - - ), + align: 'left', + width: '76px', + name: i18n.EXCEPTION_LIST_ACTIONS, + actions: [ + { + render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => ( + + ), + }, + { + render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => ( + + ), + }, + ], }, ]; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_overflow_display.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_overflow_display.tsx deleted file mode 100644 index d3219cc86d0e7..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_overflow_display.tsx +++ /dev/null @@ -1,121 +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 { EuiPopover, EuiBadgeGroup, EuiButton } from '@elastic/eui'; -import styled from 'styled-components'; -import * as i18n from '../../translations'; -import { Spacer } from '../../../../../../common/components/page'; -import { LinkAnchor } from '../../../../../../common/components/links'; -import { getRuleDetailsUrl } from '../../../../../../common/components/link_to/redirect_to_detection_engine'; -import { Rule } from '../../../../../containers/detection_engine/rules'; -import { FormatUrl } from '../../../../../../common/components/link_to'; - -interface ExceptionOverflowDisplayProps { - rules: Rule[]; - navigateToUrl: (url: string) => Promise; - formatUrl: FormatUrl; -} - -interface OverflowListComponentProps { - rule: Rule; - index: number; -} - -const ExceptionOverflowWrapper = styled(EuiBadgeGroup)` - .euiBadgeGroup__item { - display: block; - width: 100%; - } -`; - -const ExceptionOverflowPopoverWrapper = styled(EuiBadgeGroup)` - max-height: 200px; - max-width: 600px; - overflow: auto; -`; - -const ExceptionOverflowPopoverButton = styled(EuiButton)` - font-size: ${({ theme }) => theme.eui.euiFontSizeXS} - font-weight: 500; - height: 20px; -`; - -/** - * @param rules to display for filtering - */ -const ExceptionOverflowDisplayComponent = ({ - rules, - navigateToUrl, - formatUrl, -}: ExceptionOverflowDisplayProps) => { - const [isExceptionOverflowPopoverOpen, setIsExceptionOverflowPopoverOpen] = useState(false); - - const OverflowListComponent = ({ rule: { id, name }, index }: OverflowListComponentProps) => { - return ( - - void }) => { - ev.preventDefault(); - navigateToUrl(formatUrl(getRuleDetailsUrl(id))); - }} - href={formatUrl(getRuleDetailsUrl(id))} - > - {name} - - {index !== rules.length - 1 ? ', ' : ''} - - ); - }; - - return ( - <> - {rules.length <= 2 ? ( - - {rules.map((rule, index: number) => ( - - ))} - - ) : ( - - {rules.slice(0, 2).map((rule, index: number) => ( - - ))} - - setIsExceptionOverflowPopoverOpen(!isExceptionOverflowPopoverOpen)} - > - {i18n.COLUMN_SEE_ALL_POPOVER} - - } - isOpen={isExceptionOverflowPopoverOpen} - closePopover={() => setIsExceptionOverflowPopoverOpen(!isExceptionOverflowPopoverOpen)} - repositionOnScroll - > - - {rules.map((rule, index: number) => ( - - ))} - - - - )} - - ); -}; - -export const ExceptionOverflowDisplay = React.memo(ExceptionOverflowDisplayComponent); - -ExceptionOverflowDisplay.displayName = 'ExceptionOverflowDisplay'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts index 912f5bec4de35..df110d1d6ec3d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts @@ -21,6 +21,13 @@ export const EXCEPTION_LIST_NAME = i18n.translate( } ); +export const EXCEPTION_LIST_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.all.exceptions.actionsTitle', + { + defaultMessage: 'Actions', + } +); + export const NUMBER_RULES_ASSIGNED_TO_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.all.exceptions.numberRulesAssignedTitle', { @@ -35,6 +42,12 @@ export const RULES_ASSIGNED_TO_TITLE = i18n.translate( } ); +export const showMoreRules = (rulesCount: number) => + i18n.translate('xpack.securitySolution.detectionEngine.rules.all.exceptions.rulesPopoverButton', { + defaultMessage: '+{rulesCount} {rulesCount, plural, =1 {Rule} other {Rules}}', + values: { rulesCount }, + }); + export const LIST_DATE_CREATED_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.all.exceptions.dateCreatedTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/tag_display.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/tag_display.test.tsx deleted file mode 100644 index a920491d97b38..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/tag_display.test.tsx +++ /dev/null @@ -1,48 +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 from 'react'; -import { mount, ReactWrapper } from 'enzyme'; - -import { TagsDisplay } from './tag_display'; -import { TestProviders } from '../../../../../common/mock'; -import { waitFor } from '@testing-library/react'; - -const mockTags = ['Elastic', 'Endpoint', 'Data Protection', 'ML', 'Continuous Monitoring']; - -describe('When tag display loads', () => { - let wrapper: ReactWrapper; - beforeEach(() => { - wrapper = mount( - - - - ); - }); - it('visibly renders 3 initial tags', () => { - for (let i = 0; i < 3; i++) { - expect(wrapper.exists(`[data-test-subj="rules-table-column-tags-${i}"]`)).toBeTruthy(); - } - }); - describe("when the 'see all' button is clicked", () => { - beforeEach(() => { - const seeAllButton = wrapper.find('[data-test-subj="tags-display-popover-button"] button'); - seeAllButton.simulate('click'); - }); - it('renders all the tags in the popover', async () => { - await waitFor(() => { - wrapper.update(); - expect(wrapper.exists('[data-test-subj="tags-display-popover"]')).toBeTruthy(); - for (let i = 0; i < mockTags.length; i++) { - expect( - wrapper.exists(`[data-test-subj="rules-table-column-popover-tags-${i}"]`) - ).toBeTruthy(); - } - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/tag_display.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/tag_display.tsx deleted file mode 100644 index 922fd547ed53c..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/tag_display.tsx +++ /dev/null @@ -1,105 +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, { useMemo, useState } from 'react'; -import { EuiPopover, EuiBadgeGroup, EuiBadge, EuiButtonEmpty } from '@elastic/eui'; -import styled from 'styled-components'; -import * as i18n from '../translations'; -import { caseInsensitiveSort } from './helpers'; - -interface TagsDisplayProps { - tags: string[]; -} - -const TagWrapper = styled(EuiBadgeGroup)` - width: 100%; -`; - -const TagPopoverWrapper = styled(EuiBadgeGroup)` - max-height: 200px; - max-width: 600px; - overflow: auto; -`; - -const TagPopoverButton = styled(EuiButtonEmpty)` - font-size: ${({ theme }) => theme.eui.euiFontSizeXS} - font-weight: 500; - height: 20px; -`; - -/** - * @param tags to display for filtering - */ -const TagsDisplayComponent = ({ tags }: TagsDisplayProps) => { - const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false); - const sortedTags = useMemo(() => caseInsensitiveSort(tags), [tags]); - - return ( - <> - {sortedTags.length <= 3 ? ( - - {sortedTags.map((tag: string, i: number) => ( - - {tag} - - ))} - - ) : ( - - {sortedTags.slice(0, 3).map((tag: string, i: number) => ( - - {tag} - - ))} - - setIsTagPopoverOpen(!isTagPopoverOpen)} - > - {i18n.COLUMN_SEE_ALL_POPOVER} - - } - isOpen={isTagPopoverOpen} - closePopover={() => setIsTagPopoverOpen(!isTagPopoverOpen)} - repositionOnScroll - > - - {sortedTags.map((tag: string, i: number) => ( - - {tag} - - ))} - - - - )} - - ); -}; - -export const TagsDisplay = React.memo(TagsDisplayComponent); - -TagsDisplay.displayName = 'TagsDisplay'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d98d724a13f9d..6e10b6b6cdc77 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3455,8 +3455,6 @@ "kbnConfig.deprecations.replacedSettingMessage": "\"{fullOldPath}\"設定は\"{fullNewPath}\"で置換されました", "kbnConfig.deprecations.unusedSetting.manualStepOneMessage": "Kibana構成ファイル、CLIフラグ、または環境変数(Dockerのみ)から\"{fullPath}\"を削除します。", "kbnConfig.deprecations.unusedSettingMessage": "\"{fullPath}\"を構成する必要はありません。", - "kibana_legacy.notify.toaster.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}", - "kibana_legacy.notify.toaster.unavailableServerErrorMessage": "HTTP リクエストで接続に失敗しました。Kibana サーバーが実行されていて、ご使用のブラウザの接続が正常に動作していることを確認するか、システム管理者にお問い合わせください。", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", @@ -7533,6 +7531,8 @@ "xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "説明", "xpack.canvas.workpadTemplates.table.nameColumnTitle": "テンプレート名", "xpack.canvas.workpadTemplates.table.tagsColumnTitle": "タグ", + "xpack.canvas.formatMsg.toaster.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", + "xpack.canvas.formatMsg.toaster.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", "xpack.cases.addConnector.title": "コネクターの追加", "xpack.cases.allCases.actions": "アクション", "xpack.cases.allCases.comments": "コメント", @@ -19268,6 +19268,8 @@ "xpack.monitoring.updateLicenseButtonLabel": "ライセンスを更新", "xpack.monitoring.updateLicenseTitle": "ライセンスの更新", "xpack.monitoring.useAvailableLicenseDescription": "すでに新しいライセンスがある場合は、今すぐアップロードしてください。", + "xpack.monitoring.formatMsg.toaster.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", + "xpack.monitoring.formatMsg.toaster.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", "xpack.observability..synthetics.addDataButtonLabel": "Syntheticsデータの追加", "xpack.observability.alerts.manageRulesButtonLabel": "ルールの管理", "xpack.observability.alerts.searchBarPlaceholder": "検索アラート(例:kibana.alert.evaluation.threshold > 75)", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dc5ac8ea4a9be..8e25e1415521a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3480,8 +3480,6 @@ "kbnConfig.deprecations.replacedSettingMessage": "设置“{fullOldPath}”已替换为“{fullNewPath}”", "kbnConfig.deprecations.unusedSetting.manualStepOneMessage": "从 Kibana 配置文件、CLI 标志或环境变量中移除“{fullPath}”(仅适用于 Docker)。", "kbnConfig.deprecations.unusedSettingMessage": "您不再需要配置“{fullPath}”。", - "kibana_legacy.notify.toaster.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", - "kibana_legacy.notify.toaster.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "已保存对象缺失", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "无法完全还原 URL,请确保使用共享功能。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,另外,似乎没有任何可安全删除的项目。\n\n通常,这可以通过移到全新的选项卡来解决,但这种情况可能是由更大的问题造成。如果您定期看到这个消息,请在 {gitHubIssuesUrl} 报告问题。", @@ -7588,6 +7586,8 @@ "xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "描述", "xpack.canvas.workpadTemplates.table.nameColumnTitle": "模板名称", "xpack.canvas.workpadTemplates.table.tagsColumnTitle": "标签", + "xpack.canvas.formatMsg.toaster.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", + "xpack.canvas.formatMsg.toaster.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", "xpack.cases.addConnector.title": "添加连接器", "xpack.cases.allCases.actions": "操作", "xpack.cases.allCases.comments": "注释", @@ -19550,6 +19550,8 @@ "xpack.monitoring.updateLicenseButtonLabel": "更新许可证", "xpack.monitoring.updateLicenseTitle": "更新您的许可证", "xpack.monitoring.useAvailableLicenseDescription": "如果您已经持有新的许可证,请立即上传。", + "xpack.monitoring.formatMsg.toaster.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", + "xpack.monitoring.formatMsg.toaster.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", "xpack.observability..synthetics.addDataButtonLabel": "添加 Synthetics 数据", "xpack.observability.alerts.manageRulesButtonLabel": "管理规则", "xpack.observability.alerts.searchBarPlaceholder": "搜索告警(例如 kibana.alert.evaluation.threshold > 75)", diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx index 270f597cb964f..831494806e34e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx @@ -46,7 +46,7 @@ const i18nTexts = { }), pageDescription: i18n.translate('xpack.upgradeAssistant.esDeprecations.pageDescription', { defaultMessage: - 'Resolve all critical issues before upgrading. Before making changes, ensure you have a current snapshot of your cluster. Indices created before 7.0 must be reindexed or removed. To start multiple reindexing tasks in a single request, use the Kibana batch reindexing API.', + 'Resolve all critical issues before upgrading. Before making changes, ensure you have a current snapshot of your cluster. Indices created before 7.0 must be reindexed or removed.', }), isLoading: i18n.translate('xpack.upgradeAssistant.esDeprecations.loadingText', { defaultMessage: 'Loading deprecation issues…', @@ -136,8 +136,7 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { pageTitle={i18nTexts.pageTitle} description={ <> - {i18nTexts.pageDescription} - {getBatchReindexLink(docLinks)} + {i18nTexts.pageDescription} {getBatchReindexLink(docLinks)} } > diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts index 957198cde8da9..0c31a5b8d2fe5 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts @@ -64,6 +64,7 @@ describe('transformFlatSettings', () => { 'index.verified_before_close': 'true', 'index.version.created': '123123', 'index.version.upgraded': '123123', + 'index.mapper.dynamic': 'true', // Deprecated settings 'index.force_memory_term_dictionary': '1024', diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts index b65984af5deb3..870dd3ae45c30 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts @@ -178,6 +178,9 @@ const removeUnsettableSettings = (settings: FlatSettings['settings']) => 'index.verified_before_close', 'index.version.created', + // Ignored since 6.x and forbidden in 7.x + 'index.mapper.dynamic', + // Deprecated in 9.0 'index.version.upgraded', ]); diff --git a/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts b/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts index e96759c70fcae..d2a2f9a4d02dd 100644 --- a/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts +++ b/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts @@ -16,6 +16,7 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertestWithoutAuth'); const ml = getService('ml'); + const retry = getService('retry'); let notificationIndices: string[] = []; @@ -57,20 +58,25 @@ export default ({ getService }: FtrProviderContext) => { expect(body.success).to.eql(true); expect(body.last_cleared).to.be.above(timestamp); - const { body: getBody } = await supertest - .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) - .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) - .set(COMMON_REQUEST_HEADERS) - .expect(200); - - expect(getBody.messages.length).to.eql(1); - - expect(omit(getBody.messages[0], ['timestamp', 'node_name'])).to.eql({ - job_id: 'test_get_job_audit_messages_1', - message: 'Job created', - level: 'info', - job_type: 'anomaly_detector', - cleared: true, + await retry.tryForTime(5000, async () => { + const { body: getBody } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(getBody.messages.length).to.eql( + 1, + `Expected 1 job audit message, got ${JSON.stringify(getBody.messages, null, 2)}` + ); + + expect(omit(getBody.messages[0], ['timestamp', 'node_name'])).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + job_type: 'anomaly_detector', + cleared: true, + }); }); }); diff --git a/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts b/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts index c653f01c1027b..a288a7b491bb6 100644 --- a/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts +++ b/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts @@ -16,6 +16,7 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertestWithoutAuth'); const ml = getService('ml'); + const retry = getService('retry'); describe('get_job_audit_messages', function () { before(async () => { @@ -32,63 +33,81 @@ export default ({ getService }: FtrProviderContext) => { }); it('should fetch all audit messages', async () => { - const { body } = await supertest - .get(`/api/ml/job_audit_messages/messages`) - .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) - .set(COMMON_REQUEST_HEADERS) - .expect(200); - - expect(body.messages.length).to.eql(2); + await retry.tryForTime(5000, async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); - const messagesDict = keyBy(body.messages, 'job_id'); + expect(body.messages.length).to.eql( + 2, + `Expected 2 job audit messages, got ${JSON.stringify(body.messages, null, 2)}` + ); + const messagesDict = keyBy(body.messages, 'job_id'); - expect(omit(messagesDict.test_get_job_audit_messages_2, ['timestamp', 'node_name'])).to.eql({ - job_id: 'test_get_job_audit_messages_2', - message: 'Job created', - level: 'info', - job_type: 'anomaly_detector', + expect(omit(messagesDict.test_get_job_audit_messages_2, ['timestamp', 'node_name'])).to.eql( + { + job_id: 'test_get_job_audit_messages_2', + message: 'Job created', + level: 'info', + job_type: 'anomaly_detector', + } + ); + expect(omit(messagesDict.test_get_job_audit_messages_1, ['timestamp', 'node_name'])).to.eql( + { + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + job_type: 'anomaly_detector', + } + ); + expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); }); - expect(omit(messagesDict.test_get_job_audit_messages_1, ['timestamp', 'node_name'])).to.eql({ - job_id: 'test_get_job_audit_messages_1', - message: 'Job created', - level: 'info', - job_type: 'anomaly_detector', - }); - expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); }); it('should fetch audit messages for specified job', async () => { - const { body } = await supertest - .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) - .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) - .set(COMMON_REQUEST_HEADERS) - .expect(200); + await retry.tryForTime(5000, async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); - expect(body.messages.length).to.eql(1); - expect(omit(body.messages[0], ['timestamp', 'node_name'])).to.eql({ - job_id: 'test_get_job_audit_messages_1', - message: 'Job created', - level: 'info', - job_type: 'anomaly_detector', + expect(body.messages.length).to.eql( + 1, + `Expected 1 job audit message, got ${JSON.stringify(body.messages, null, 2)}` + ); + expect(omit(body.messages[0], ['timestamp', 'node_name'])).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + job_type: 'anomaly_detector', + }); + expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); }); - expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); }); it('should fetch audit messages for user with ML read permissions', async () => { - const { body } = await supertest - .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) - .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) - .set(COMMON_REQUEST_HEADERS) - .expect(200); + await retry.tryForTime(5000, async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); - expect(body.messages.length).to.eql(1); - expect(omit(body.messages[0], ['timestamp', 'node_name'])).to.eql({ - job_id: 'test_get_job_audit_messages_1', - message: 'Job created', - level: 'info', - job_type: 'anomaly_detector', + expect(body.messages.length).to.eql( + 1, + `Expected 1 job audit message, got ${JSON.stringify(body.messages, null, 2)}` + ); + expect(omit(body.messages[0], ['timestamp', 'node_name'])).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + job_type: 'anomaly_detector', + }); + expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); }); - expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); }); it('should not allow to fetch audit messages for unauthorized user', async () => { diff --git a/x-pack/test/functional_execution_context/config.ts b/x-pack/test/functional_execution_context/config.ts index 456d31b586ad0..6c7935593a18d 100644 --- a/x-pack/test/functional_execution_context/config.ts +++ b/x-pack/test/functional_execution_context/config.ts @@ -55,6 +55,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { '--logging.loggers[1].name=execution_context', '--logging.loggers[1].level=debug', `--logging.loggers[1].appenders=${JSON.stringify(['file'])}`, + + '--logging.loggers[2].name=http.server.response', + '--logging.loggers[2].level=all', + `--logging.loggers[2].appenders=${JSON.stringify(['file'])}`, ], }, }; diff --git a/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts index ec4e3ef99c6df..20e0adbf5dacd 100644 --- a/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts @@ -6,7 +6,7 @@ */ import apmAgent from 'elastic-apm-node'; -import { Plugin, CoreSetup } from 'kibana/server'; +import type { Plugin, CoreSetup } from 'kibana/server'; import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../plugins/alerting/server/plugin'; import { EncryptedSavedObjectsPluginStart } from '../../../../../../plugins/encrypted_saved_objects/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../plugins/features/server'; @@ -93,10 +93,14 @@ export class FixturePlugin implements Plugin { + // Kibana might set transactiopnSampleRate < 1.0 on CI, so we need to + // enforce transaction creation to prevent the test from failing. const transaction = apmAgent.startTransaction(); const subscription = req.events.completed$.subscribe(() => { - transaction?.end(); - subscription.unsubscribe(); + setTimeout(() => { + transaction?.end(); + subscription.unsubscribe(); + }, 1_000); }); await ctx.core.elasticsearch.client.asInternalUser.ping(); diff --git a/x-pack/test/functional_execution_context/tests/log_correlation.ts b/x-pack/test/functional_execution_context/tests/log_correlation.ts index 80bb2285a665e..fddaf282b7a7f 100644 --- a/x-pack/test/functional_execution_context/tests/log_correlation.ts +++ b/x-pack/test/functional_execution_context/tests/log_correlation.ts @@ -14,23 +14,46 @@ export default function ({ getService }: FtrProviderContext) { describe('Log Correlation', () => { it('Emits "trace.id" into the logs', async () => { - const response1 = await supertest - .get('/emit_log_with_trace_id') - .set('x-opaque-id', 'myheader1'); - + const response1 = await supertest.get('/emit_log_with_trace_id'); + expect(response1.status).to.be(200); expect(response1.body.traceId).to.be.a('string'); const response2 = await supertest.get('/emit_log_with_trace_id'); + expect(response2.status).to.be(200); expect(response1.body.traceId).to.be.a('string'); expect(response2.body.traceId).not.to.be(response1.body.traceId); + let responseTraceId: string | undefined; await assertLogContains({ - description: 'traceId included in the Kibana logs', - predicate: (record) => + description: 'traceId included in the http logs', + predicate: (record) => { // we don't check trace.id value since trace.id in the test plugin and Kibana are different on CI. // because different 'elastic-apm-node' instaces are imported - Boolean(record.http?.request?.id?.includes('myheader1') && record.trace?.id), + if ( + record.log?.logger === 'http.server.response' && + record.url?.path === '/emit_log_with_trace_id' + ) { + responseTraceId = record.trace?.id; + return true; + } + return false; + }, + retry, + }); + + expect(responseTraceId).to.be.a('string'); + + await assertLogContains({ + description: 'elasticsearch logs have the same traceId', + predicate: (record) => + Boolean( + record.log?.logger === 'elasticsearch.query.data' && + record.trace?.id === responseTraceId && + // esClient.ping() request + record.message?.includes('HEAD /') + ), + retry, }); });