diff --git a/dev_docs/api_welcome.mdx b/dev_docs/api_welcome.mdx new file mode 100644 index 0000000000000..692a1bb2e582e --- /dev/null +++ b/dev_docs/api_welcome.mdx @@ -0,0 +1,14 @@ +--- +id: kibDevDocsApiWelcome +slug: /kibana-dev-docs/api-welcome +title: Welcome +summary: The home of automatically generated plugin API documentation using extracted TSDocs +date: 2021-01-02 +tags: ['kibana','dev', 'contributor', 'api docs'] +--- + +Welcome to Kibana's plugin API documentation. As a plugin developer, this is where you can +learn the details of every service you can take advantage of to help you build awe-inspiring creative solutions and applications! + +If you have any questions or issues, please reach out to the Kibana platform team or create an issue [here](https://github.com/elastic/kibana/issues). + diff --git a/dev_docs/dev_welcome.mdx b/dev_docs/dev_welcome.mdx new file mode 100644 index 0000000000000..cc185e689fa43 --- /dev/null +++ b/dev_docs/dev_welcome.mdx @@ -0,0 +1,17 @@ +--- +id: kibDevDocsWelcome +slug: /kibana-dev-docs/welcome +title: Welcome +summary: Build custom solutions and applications on top of Kibana. +date: 2021-01-02 +tags: ['kibana','dev', 'contributor'] +--- + +Welcome to Kibana's plugin developer documentation! + +Did you know that the vast majority of functionality built inside of Kibana is a plugin? A handful of core services hold the system together, +but it's our vast system of plugin developers that provide the amazing, out of the box, functionality you can use when building your own set of +custom utilities and applications. + +Browse the `Services` section to view all the plugins that offer functionality you can take advantage of, or check out the +`API documentation` to dig into the nitty gritty details of every public plugin API. diff --git a/docs/settings/settings-xkb.asciidoc b/docs/settings/settings-xkb.asciidoc index 9d9cc92401896..4a211976be8cf 100644 --- a/docs/settings/settings-xkb.asciidoc +++ b/docs/settings/settings-xkb.asciidoc @@ -19,5 +19,6 @@ include::logs-ui-settings.asciidoc[] include::ml-settings.asciidoc[] include::reporting-settings.asciidoc[] include::spaces-settings.asciidoc[] +include::task-manager-settings.asciidoc[] include::i18n-settings.asciidoc[] include::fleet-settings.asciidoc[] diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc new file mode 100644 index 0000000000000..507e54349276b --- /dev/null +++ b/docs/settings/task-manager-settings.asciidoc @@ -0,0 +1,32 @@ +[role="xpack"] +[[task-manager-settings-kb]] +=== Task Manager settings in {kib} +++++ +Task Manager settings +++++ + +Task Manager runs background tasks by polling for work on an interval. You can configure its behavior to tune for performance and throughput. + +[float] +[[task-manager-settings]] +==== Task Manager settings + +[cols="2*<"] +|=== +| `xpack.task_manager.max_attempts` + | The maximum number of times a task will be attempted before being abandoned as failed. Defaults to 3. + +| `xpack.task_manager.poll_interval` + | How often, in milliseconds, the task manager will look for more work. Defaults to 3000 and cannot be lower than 100. + +| `xpack.task_manager.request_capacity` + | How many requests can Task Manager buffer before it rejects new requests. Defaults to 1000. + +| `xpack.task_manager.index` + | The name of the index used to store task information. Defaults to `.kibana_task_manager`. + + | `xpack.task_manager.max_workers` + | The maximum number of tasks that this Kibana instance will run simultaneously. Defaults to 10. + + +|=== diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 0dee112d15e86..5d79a81e0aa91 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -129,7 +129,7 @@ images: [horizontal] `server.name`:: `kibana` -`server.host`:: `"0"` +`server.host`:: `"0.0.0.0"` `elasticsearch.hosts`:: `http://elasticsearch:9200` `monitoring.ui.container.elasticsearch.enabled`:: `true` diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index cb2b9b19a0726..06370c64aedf8 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -57,7 +57,7 @@ Alert schedules are defined as an interval between subsequent checks, and can ra [IMPORTANT] ============================================== -The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. +The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. ============================================== [float] diff --git a/docs/user/alerting/alerting-scale-performance.asciidoc b/docs/user/alerting/alerting-production-considerations.asciidoc similarity index 65% rename from docs/user/alerting/alerting-scale-performance.asciidoc rename to docs/user/alerting/alerting-production-considerations.asciidoc index 644a7143f8278..3a68e81879e24 100644 --- a/docs/user/alerting/alerting-scale-performance.asciidoc +++ b/docs/user/alerting/alerting-production-considerations.asciidoc @@ -1,10 +1,10 @@ [role="xpack"] -[[alerting-scale-performance]] -== Scale and performance +[[alerting-production-considerations]] +== Production considerations -{kib} alerting run both alert checks and actions as persistent background tasks. This has two major benefits: +{kib} alerting run both alert checks and actions as persistent background tasks managed by the Kibana Task Manager. This has two major benefits: -* *Persistence*: all task state and scheduling is stored in {es}, so if {kib} is restarted, alerts and actions will pick up where they left off. +* *Persistence*: all task state and scheduling is stored in {es}, so if {kib} is restarted, alerts and actions will pick up where they left off. Task definitions for alerts and actions are stored in the index specified by `xpack.task_manager.index` (defaults to `.kibana_task_manager`). It is important to have at least 1 replica of this index for production deployments, since if you lose this index all scheduled alerts and actions are also lost. * *Scaling*: multiple {kib} instances can read from and update the same task queue in {es}, allowing the alerting and action load to be distributed across instances. In cases where a {kib} instance no longer has capacity to run alert checks or actions, capacity can be increased by adding additional {kib} instances. [float] @@ -12,17 +12,19 @@ {kib} background tasks are managed by: -* Polling an {es} task index for overdue tasks at 3 second intervals. +* Polling an {es} task index for overdue tasks at 3 second intervals. This interval can be changed using the `xpack.task_manager.poll_interval` setting. * Tasks are then claiming them by updating them in the {es} index, using optimistic concurrency control to prevent conflicts. Each {kib} instance can run a maximum of 10 concurrent tasks, so a maximum of 10 tasks are claimed each interval. * Tasks are run on the {kib} server. * In the case of alerts which are recurring background checks, upon completion the task is scheduled again according to the <>. [IMPORTANT] ============================================== -Because tasks are polled at 3 second intervals and only 10 tasks can run concurrently per {kib} instance, it is possible for alert and action tasks to be run late. This can happen if: +Because by default tasks are polled at 3 second intervals and only 10 tasks can run concurrently per {kib} instance, it is possible for alert and action tasks to be run late. This can happen if: * Alerts use a small *check interval*. The lowest interval possible is 3 seconds, though intervals of 30 seconds or higher are recommended. * Many alerts or actions must be *run at once*. In this case pending tasks will queue in {es}, and be pulled 10 at a time from the queue at 3 second intervals. * *Long running tasks* occupy slots for an extended time, leaving fewer slots for other tasks. +For details on the settings that can influence the performance and throughput of Task Manager, see {task-manager-settings}. + ============================================== \ No newline at end of file diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index ffd72cc824336..94cca7f91494e 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -26,7 +26,7 @@ image::images/alert-flyout-general-details.png[alt='All alerts have name, tags, Name:: The name of the alert. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable alert listing in the management UI. A distinctive name can help identify and find an alert. Tags:: A list of tag names that can be applied to an alert. Tags can help you organize and find alerts, because tags appear in the alert listing in the management UI which is searchable by tag. -Check every:: This value determines how frequently the alert conditions below are checked. Note that the timing of background alert checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. +Check every:: This value determines how frequently the alert conditions below are checked. Note that the timing of background alert checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. Notify every:: This value limits how often actions are repeated when an alert instance remains active across alert checks. See <> for more information. [float] diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 56404d9a33b80..caef0c6e7332d 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -2,4 +2,4 @@ include::alerting-getting-started.asciidoc[] include::defining-alerts.asciidoc[] include::action-types.asciidoc[] include::alert-types.asciidoc[] -include::alerting-scale-performance.asciidoc[] +include::alerting-production-considerations.asciidoc[] diff --git a/src/core/server/core_app/assets/favicons/android-chrome-192x192.png b/src/core/server/core_app/assets/favicons/android-chrome-192x192.png deleted file mode 100644 index 18a86e5b95c46..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/android-chrome-192x192.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/android-chrome-256x256.png b/src/core/server/core_app/assets/favicons/android-chrome-256x256.png deleted file mode 100644 index 8238d772ce40b..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/android-chrome-256x256.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/apple-touch-icon.png b/src/core/server/core_app/assets/favicons/apple-touch-icon.png deleted file mode 100644 index 1ffeb0852a170..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/apple-touch-icon.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/browserconfig.xml b/src/core/server/core_app/assets/favicons/browserconfig.xml deleted file mode 100644 index b3930d0f04718..0000000000000 --- a/src/core/server/core_app/assets/favicons/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #da532c - - - diff --git a/src/core/server/core_app/assets/favicons/favicon-16x16.png b/src/core/server/core_app/assets/favicons/favicon-16x16.png deleted file mode 100644 index 631f5b7c7d74b..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/favicon-16x16.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/favicon-32x32.png b/src/core/server/core_app/assets/favicons/favicon-32x32.png deleted file mode 100644 index bf94dfa995f37..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/favicon-32x32.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/favicon.distribution.png b/src/core/server/core_app/assets/favicons/favicon.distribution.png new file mode 100644 index 0000000000000..9be046aba59b6 Binary files /dev/null and b/src/core/server/core_app/assets/favicons/favicon.distribution.png differ diff --git a/src/core/server/core_app/assets/favicons/favicon.distribution.svg b/src/core/server/core_app/assets/favicons/favicon.distribution.svg new file mode 100644 index 0000000000000..2d02461a0b8f9 --- /dev/null +++ b/src/core/server/core_app/assets/favicons/favicon.distribution.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/core/server/core_app/assets/favicons/favicon.ico b/src/core/server/core_app/assets/favicons/favicon.ico deleted file mode 100644 index db30798a6cf32..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/favicon.ico and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/favicon.png b/src/core/server/core_app/assets/favicons/favicon.png new file mode 100644 index 0000000000000..cba7a268c6c59 Binary files /dev/null and b/src/core/server/core_app/assets/favicons/favicon.png differ diff --git a/src/core/server/core_app/assets/favicons/favicon.svg b/src/core/server/core_app/assets/favicons/favicon.svg new file mode 100644 index 0000000000000..4ae6524bf0d18 --- /dev/null +++ b/src/core/server/core_app/assets/favicons/favicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/core/server/core_app/assets/favicons/manifest.json b/src/core/server/core_app/assets/favicons/manifest.json deleted file mode 100644 index de65106f489b7..0000000000000 --- a/src/core/server/core_app/assets/favicons/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-256x256.png", - "sizes": "256x256", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/src/core/server/core_app/assets/favicons/mstile-150x150.png b/src/core/server/core_app/assets/favicons/mstile-150x150.png deleted file mode 100644 index 82769c1ef242b..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/mstile-150x150.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg b/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg deleted file mode 100644 index 38a64142be0b7..0000000000000 --- a/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - - - - - - diff --git a/src/core/server/core_app/integration_tests/static_assets.test.ts b/src/core/server/core_app/integration_tests/static_assets.test.ts index ca03c4228221f..45e7b79b5d5e6 100644 --- a/src/core/server/core_app/integration_tests/static_assets.test.ts +++ b/src/core/server/core_app/integration_tests/static_assets.test.ts @@ -34,11 +34,11 @@ describe('Platform assets', function () { }); it('exposes static assets', async () => { - await kbnTestServer.request.get(root, '/ui/favicons/favicon.ico').expect(200); + await kbnTestServer.request.get(root, '/ui/favicons/favicon.svg').expect(200); }); it('returns 404 if not found', async function () { - await kbnTestServer.request.get(root, '/ui/favicons/not-a-favicon.ico').expect(404); + await kbnTestServer.request.get(root, '/ui/favicons/not-a-favicon.svg').expect(404); }); it('does not expose folder content', async function () { diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 9b667f888771e..4545396c27b5e 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -24,6 +24,12 @@ Object { } `; +exports[`accepts valid hostnames 5`] = ` +Object { + "host": "0.0.0.0", +} +`; + exports[`basePath throws if appends a slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; exports[`basePath throws if is an empty string 1`] = `"[basePath]: must start with a slash, don't end with one"`; @@ -105,6 +111,8 @@ Object { exports[`throws if invalid hostname 1`] = `"[host]: value must be a valid hostname (see RFC 1123)."`; +exports[`throws if invalid hostname 2`] = `"[host]: value 0 is not a valid hostname (use \\"0.0.0.0\\" to bind to all interfaces)"`; + exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`; exports[`with compression accepts valid referrer whitelist 1`] = ` @@ -113,6 +121,7 @@ Array [ "8.8.8.8", "::1", "localhost", + "0.0.0.0", ] `; diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index b71763e8a2e14..b1b2ba5b295a7 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -22,8 +22,8 @@ import { config, HttpConfig } from './http_config'; import { CspConfig } from '../csp'; import { ExternalUrlConfig } from '../external_url'; -const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost']; -const invalidHostname = 'asdf$%^'; +const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost', '0.0.0.0']; +const invalidHostnames = ['asdf$%^', '0']; jest.mock('os', () => { const original = jest.requireActual('os'); @@ -48,11 +48,10 @@ test('accepts valid hostnames', () => { }); test('throws if invalid hostname', () => { - const httpSchema = config.schema; - const obj = { - host: invalidHostname, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + for (const host of invalidHostnames) { + const httpSchema = config.schema; + expect(() => httpSchema.validate({ host })).toThrowErrorMatchingSnapshot(); + } }); describe('requestId', () => { @@ -304,9 +303,9 @@ describe('with compression', () => { test('throws if invalid referrer whitelist', () => { const httpSchema = config.schema; - const invalidHostnames = { + const nonEmptyArray = { compression: { - referrerWhitelist: [invalidHostname], + referrerWhitelist: invalidHostnames, }, }; const emptyArray = { @@ -314,7 +313,7 @@ describe('with compression', () => { referrerWhitelist: [], }, }; - expect(() => httpSchema.validate(invalidHostnames)).toThrowErrorMatchingSnapshot(); + expect(() => httpSchema.validate(nonEmptyArray)).toThrowErrorMatchingSnapshot(); expect(() => httpSchema.validate(emptyArray)).toThrowErrorMatchingSnapshot(); }); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 61a9b5f04b23f..aa4db6f88d338 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -73,6 +73,11 @@ export const config = { host: schema.string({ defaultValue: 'localhost', hostname: true, + validate(value) { + if (value === '0') { + return 'value 0 is not a valid hostname (use "0.0.0.0" to bind to all interfaces)'; + } + }, }), maxPayload: schema.byteSize({ defaultValue: '1048576b', @@ -195,13 +200,7 @@ export class HttpConfig { rawExternalUrlConfig: ExternalUrlConfig ) { this.autoListen = rawHttpConfig.autoListen; - // TODO: Consider dropping support for '0' in v8.0.0. This value is passed - // to hapi, which validates it. Prior to hapi v20, '0' was considered a - // valid host, however the validation logic internally in hapi was - // re-written for v20 and hapi no longer considers '0' a valid host. For - // details, see: - // https://github.com/elastic/kibana/issues/86716#issuecomment-749623781 - this.host = rawHttpConfig.host === '0' ? '0.0.0.0' : rawHttpConfig.host; + this.host = rawHttpConfig.host; this.port = rawHttpConfig.port; this.cors = rawHttpConfig.cors; this.customResponseHeaders = Object.entries(rawHttpConfig.customResponseHeaders ?? {}).reduce( diff --git a/src/core/server/rendering/views/template.tsx b/src/core/server/rendering/views/template.tsx index 76af229ac02ba..e4787ee26e12c 100644 --- a/src/core/server/rendering/views/template.tsx +++ b/src/core/server/rendering/views/template.tsx @@ -76,33 +76,11 @@ export const Template: FunctionComponent = ({ Elastic - {/* Favicons (generated from http://realfavicongenerator.net/) */} - - - - - - - + {/* The alternate icon is a fallback for Safari which does not yet support SVG favicons */} + + + {/* Inject stylesheets into the before scripts so that KP plugins with bundled styles will override them */} diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index 77f49e336a7b9..294f716036f12 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -79,17 +79,18 @@ describe('#importSavedObjectsFromStream', () => { let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; - const setupOptions = (createNewCopies: boolean = false): SavedObjectsImportOptions => { + const setupOptions = ( + createNewCopies: boolean = false, + getTypeImpl: (name: string) => any = (type: string) => + ({ + // other attributes aren't needed for the purposes of injecting metadata + management: { icon: `${type}-icon` }, + } as any) + ): SavedObjectsImportOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); - typeRegistry.getType.mockImplementation( - (type: string) => - ({ - // other attributes aren't needed for the purposes of injecting metadata - management: { icon: `${type}-icon` }, - } as any) - ); + typeRegistry.getType.mockImplementation(getTypeImpl); return { readStream, objectLimit, @@ -100,14 +101,17 @@ describe('#importSavedObjectsFromStream', () => { createNewCopies, }; }; - const createObject = (): SavedObject<{ + const createObject = ({ + type = 'foo-type', + title = 'some-title', + }: { type?: string; title?: string } = {}): SavedObject<{ title: string; }> => { return { - type: 'foo-type', + type, id: uuidv4(), references: [], - attributes: { title: 'some-title' }, + attributes: { title }, }; }; const createError = (): SavedObjectsImportError => { @@ -419,6 +423,51 @@ describe('#importSavedObjectsFromStream', () => { }); }); + test('uses `type.management.getTitle` to resolve the titles', async () => { + const obj1 = createObject({ type: 'foo' }); + const obj2 = createObject({ type: 'bar', title: 'bar-title' }); + + const options = setupOptions(false, (type) => { + if (type === 'foo') { + return { + management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, + }; + } + return { + management: { icon: `${type}-icon` }, + }; + }); + + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects: [], + importIdMap: new Map(), + pendingOverwrites: new Set(), + }); + getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [obj1, obj2] }); + + const result = await importSavedObjectsFromStream(options); + // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) + const successResults = [ + { + type: obj1.type, + id: obj1.id, + meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, + }, + { + type: obj2.type, + id: obj2.id, + meta: { title: 'bar-title', icon: `${obj2.type}-icon` }, + }, + ]; + + expect(result).toEqual({ + success: true, + successCount: 2, + successResults, + }); + }); + test('accumulates multiple errors', async () => { const options = setupOptions(); const errors = [createError(), createError(), createError(), createError(), createError()]; diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index 4530c7ff427da..fd169e92cc89a 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -111,20 +111,23 @@ export async function importSavedObjectsFromStream({ const createSavedObjectsResult = await createSavedObjects(createSavedObjectsParams); errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; - const successResults = createSavedObjectsResult.createdObjects.map( - ({ type, id, attributes: { title }, destinationId, originId }) => { - const meta = { title, icon: typeRegistry.getType(type)?.management?.icon }; - const attemptedOverwrite = pendingOverwrites.has(`${type}:${id}`); - return { - type, - id, - meta, - ...(attemptedOverwrite && { overwrite: true }), - ...(destinationId && { destinationId }), - ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), - }; - } - ); + const successResults = createSavedObjectsResult.createdObjects.map((createdObject) => { + const { type, id, destinationId, originId } = createdObject; + const getTitle = typeRegistry.getType(type)?.management?.getTitle; + const meta = { + title: getTitle ? getTitle(createdObject) : createdObject.attributes.title, + icon: typeRegistry.getType(type)?.management?.icon, + }; + const attemptedOverwrite = pendingOverwrites.has(`${type}:${id}`); + return { + type, + id, + meta, + ...(attemptedOverwrite && { overwrite: true }), + ...(destinationId && { destinationId }), + ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), + }; + }); const errorResults = errorAccumulator.map((error) => { const icon = typeRegistry.getType(error.type)?.management?.icon; const attemptedOverwrite = pendingOverwrites.has(`${error.type}:${error.id}`); diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts index 51a48dc511e2a..a9dd00eb4ce92 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts @@ -89,18 +89,18 @@ describe('#importSavedObjectsFromStream', () => { const setupOptions = ( retries: SavedObjectsImportRetry[] = [], - createNewCopies: boolean = false + createNewCopies: boolean = false, + getTypeImpl: (name: string) => any = (type: string) => + ({ + // other attributes aren't needed for the purposes of injecting metadata + management: { icon: `${type}-icon` }, + } as any) ): SavedObjectsResolveImportErrorsOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); - typeRegistry.getType.mockImplementation( - (type: string) => - ({ - // other attributes aren't needed for the purposes of injecting metadata - management: { icon: `${type}-icon` }, - } as any) - ); + typeRegistry.getType.mockImplementation(getTypeImpl); + return { readStream, objectLimit, @@ -122,15 +122,16 @@ describe('#importSavedObjectsFromStream', () => { return { type: 'foo-type', id, overwrite, replaceReferences }; }; const createObject = ( - references?: SavedObjectReference[] + references?: SavedObjectReference[], + { type = 'foo-type', title = 'some-title' }: { type?: string; title?: string } = {} ): SavedObject<{ title: string; }> => { return { - type: 'foo-type', + type, id: uuidv4(), references: references || [], - attributes: { title: 'some-title' }, + attributes: { title }, }; }; const createError = (): SavedObjectsImportError => { @@ -267,7 +268,7 @@ describe('#importSavedObjectsFromStream', () => { expect(getImportIdMapForRetries).toHaveBeenCalledWith(getImportIdMapForRetriesParams); }); - test('splits objects to ovewrite from those not to overwrite', async () => { + test('splits objects to overwrite from those not to overwrite', async () => { const retries = [createRetry()]; const options = setupOptions(retries); const collectedObjects = [createObject()]; @@ -491,6 +492,55 @@ describe('#importSavedObjectsFromStream', () => { expect(result).toEqual({ success: false, successCount: 3, successResults, errors }); }); + test('uses `type.management.getTitle` to resolve the titles', async () => { + const obj1 = createObject([], { type: 'foo' }); + const obj2 = createObject([], { type: 'bar', title: 'bar-title' }); + + const options = setupOptions([], false, (type) => { + if (type === 'foo') { + return { + management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, + }; + } + return { + management: { icon: `${type}-icon` }, + }; + }); + + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects: [], + importIdMap: new Map(), + pendingOverwrites: new Set(), + }); + getMockFn(createSavedObjects) + .mockResolvedValueOnce({ errors: [], createdObjects: [obj1, obj2] }) + .mockResolvedValueOnce({ errors: [], createdObjects: [] }); + + const result = await resolveSavedObjectsImportErrors(options); + // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) + const successResults = [ + { + type: obj1.type, + id: obj1.id, + overwrite: true, + meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, + }, + { + type: obj2.type, + id: obj2.id, + overwrite: true, + meta: { title: 'bar-title', icon: `${obj2.type}-icon` }, + }, + ]; + + expect(result).toEqual({ + success: true, + successCount: 2, + successResults, + }); + }); + test('accumulates multiple errors', async () => { const options = setupOptions(); const errors = [createError(), createError(), createError(), createError()]; diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts index 2182d9252cd51..e1d7075b9371b 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -153,8 +153,13 @@ export async function resolveSavedObjectsImportErrors({ successCount += createdObjects.length; successResults = [ ...successResults, - ...createdObjects.map(({ type, id, attributes: { title }, destinationId, originId }) => { - const meta = { title, icon: typeRegistry.getType(type)?.management?.icon }; + ...createdObjects.map((createdObject) => { + const { type, id, destinationId, originId } = createdObject; + const getTitle = typeRegistry.getType(type)?.management?.getTitle; + const meta = { + title: getTitle ? getTitle(createdObject) : createdObject.attributes.title, + icon: typeRegistry.getType(type)?.management?.icon, + }; return { type, id, diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 1081d5d0d6dbd..4613303808f8e 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -61,6 +61,7 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions */ await run(Tasks.CopySource); await run(Tasks.CopyBinScripts); + await run(Tasks.ReplaceFavicon); await run(Tasks.CreateEmptyDirsAndFiles); await run(Tasks.CreateReadme); await run(Tasks.BuildPackages); diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 710e504e58868..038ccba5ed17e 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -31,6 +31,8 @@ export const CopySource: Task = { '!src/**/*.{test,test.mocks,mock}.{js,ts,tsx}', '!src/**/mocks.ts', // special file who imports .mock files '!src/**/{target,__tests__,__snapshots__,__mocks__}/**', + '!src/core/server/core_app/assets/favicons/favicon.distribution.png', + '!src/core/server/core_app/assets/favicons/favicon.distribution.svg', '!src/test_utils/**', '!src/fixtures/**', '!src/cli/repl/**', diff --git a/src/dev/build/tasks/index.ts b/src/dev/build/tasks/index.ts index ec0de7ca84aad..ca10fcca80498 100644 --- a/src/dev/build/tasks/index.ts +++ b/src/dev/build/tasks/index.ts @@ -38,6 +38,7 @@ export * from './transpile_babel_task'; export * from './uuid_verification_task'; export * from './verify_env_task'; export * from './write_sha_sums_task'; +export * from './replace_favicon'; // @ts-expect-error this module can't be TS because it ends up pulling x-pack into Kibana export { InstallChromium } from './install_chromium'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index 240ec6f4e9326..a849c6bf4992d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -29,7 +29,7 @@ function generator({ imageFlavor }: TemplateContext) { # Default Kibana configuration for docker target server.name: kibana - server.host: "0" + server.host: "0.0.0.0" elasticsearch.hosts: [ "http://elasticsearch:9200" ] ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} `); diff --git a/src/dev/build/tasks/replace_favicon.ts b/src/dev/build/tasks/replace_favicon.ts new file mode 100644 index 0000000000000..bdf5764b0f4e7 --- /dev/null +++ b/src/dev/build/tasks/replace_favicon.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { copy, Task } from '../lib'; + +export const ReplaceFavicon: Task = { + description: 'Replacing favicons with built version', + + async run(config, log, build) { + await copy( + config.resolveFromRepo('src/core/server/core_app/assets/favicons/favicon.distribution.png'), + build.resolvePath('src/core/server/core_app/assets/favicons/favicon.png') + ); + + await copy( + config.resolveFromRepo('src/core/server/core_app/assets/favicons/favicon.distribution.svg'), + build.resolvePath('src/core/server/core_app/assets/favicons/favicon.svg') + ); + }, +}; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js b/src/plugins/console/public/application/models/legacy_core_editor/input.test.js similarity index 98% rename from src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js rename to src/plugins/console/public/application/models/legacy_core_editor/input.test.js index 81171c2bd26fe..f7b618aefd6fd 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/input.test.js @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import '../legacy_core_editor.test.mocks'; -import RowParser from '../../../../lib/row_parser'; -import { createTokenIterator } from '../../../factories'; +import './legacy_core_editor.test.mocks'; +import RowParser from '../../../lib/row_parser'; +import { createTokenIterator } from '../../factories'; import $ from 'jquery'; -import { create } from '../create'; +import { create } from './create'; describe('Input', () => { let coreEditor; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js similarity index 94% rename from src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js rename to src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js index ea7530bd21387..aa6b03e5ae290 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import '../legacy_core_editor.test.mocks'; +import './legacy_core_editor.test.mocks'; import $ from 'jquery'; -import RowParser from '../../../../lib/row_parser'; +import RowParser from '../../../lib/row_parser'; import ace from 'brace'; -import { createReadOnlyAceEditor } from '../create_readonly'; +import { createReadOnlyAceEditor } from './create_readonly'; let output; const tokenIterator = ace.acequire('ace/token_iterator'); diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/editor_input1.txt b/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt similarity index 100% rename from src/plugins/console/public/application/models/sense_editor/__tests__/editor_input1.txt rename to src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js b/src/plugins/console/public/application/models/sense_editor/integration.test.js similarity index 99% rename from src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js rename to src/plugins/console/public/application/models/sense_editor/integration.test.js index 89880528943e5..5caf772f04c39 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/integration.test.js @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import '../sense_editor.test.mocks'; -import { create } from '../create'; +import './sense_editor.test.mocks'; +import { create } from './create'; import _ from 'lodash'; import $ from 'jquery'; -import * as kb from '../../../../lib/kb/kb'; -import * as mappings from '../../../../lib/mappings/mappings'; +import * as kb from '../../../lib/kb/kb'; +import * as mappings from '../../../lib/mappings/mappings'; describe('Integration', () => { let senseEditor; diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js similarity index 98% rename from src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js rename to src/plugins/console/public/application/models/sense_editor/sense_editor.test.js index 04d3cd1a724e1..d1bc4bdd62116 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js @@ -16,14 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import '../sense_editor.test.mocks'; +import './sense_editor.test.mocks'; import $ from 'jquery'; import _ from 'lodash'; -import { create } from '../create'; -import { XJson } from '../../../../../../es_ui_shared/public'; -import editorInput1 from './editor_input1.txt'; +import { create } from './create'; +import { XJson } from '../../../../../es_ui_shared/public'; +import editorInput1 from './__fixtures__/editor_input1.txt'; const { collapseLiteralStrings } = XJson; diff --git a/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js b/src/plugins/console/public/lib/autocomplete/url_autocomplete.test.js similarity index 98% rename from src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js rename to src/plugins/console/public/lib/autocomplete/url_autocomplete.test.js index 0f97416f053ee..4d2692b3ba16c 100644 --- a/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js +++ b/src/plugins/console/public/lib/autocomplete/url_autocomplete.test.js @@ -18,13 +18,8 @@ */ import _ from 'lodash'; -import { - URL_PATH_END_MARKER, - UrlPatternMatcher, - ListComponent, -} from '../../autocomplete/components'; - -import { populateContext } from '../../autocomplete/engine'; +import { URL_PATH_END_MARKER, UrlPatternMatcher, ListComponent } from './components'; +import { populateContext } from './engine'; describe('Url autocomplete', () => { function patternsTest(name, endpoints, tokenPath, expectedContext, globalUrlComponentFactories) { diff --git a/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js b/src/plugins/console/public/lib/autocomplete/url_params.test.js similarity index 96% rename from src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js rename to src/plugins/console/public/lib/autocomplete/url_params.test.js index e624e7ba57b61..d74d9c1c159bd 100644 --- a/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js +++ b/src/plugins/console/public/lib/autocomplete/url_params.test.js @@ -17,8 +17,8 @@ * under the License. */ import _ from 'lodash'; -import { UrlParams } from '../../autocomplete/url_params'; -import { populateContext } from '../../autocomplete/engine'; +import { UrlParams } from './url_params'; +import { populateContext } from './engine'; describe('Url params', () => { function paramTest(name, description, tokenPath, expectedContext, globalParams) { diff --git a/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.txt b/src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt similarity index 100% rename from src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.txt rename to src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt diff --git a/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js b/src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js similarity index 93% rename from src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js rename to src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js index 068dd68be4ba8..6f4e531715f7f 100644 --- a/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js +++ b/src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js @@ -18,8 +18,8 @@ */ import _ from 'lodash'; -import { detectCURL, parseCURL } from '../curl'; -import curlTests from './curl_parsing.txt'; +import { detectCURL, parseCURL } from './curl'; +import curlTests from './__fixtures__/curl_parsing.txt'; describe('CURL', () => { const notCURLS = ['sldhfsljfhs', 's;kdjfsldkfj curl -XDELETE ""', '{ "hello": 1 }']; diff --git a/src/plugins/console/public/lib/es/__tests__/content_type.test.js b/src/plugins/console/public/lib/es/content_type.test.js similarity index 96% rename from src/plugins/console/public/lib/es/__tests__/content_type.test.js rename to src/plugins/console/public/lib/es/content_type.test.js index e800fe41cb018..af62a3cad3f1f 100644 --- a/src/plugins/console/public/lib/es/__tests__/content_type.test.js +++ b/src/plugins/console/public/lib/es/content_type.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { getContentType } from '../es'; +import { getContentType } from './es'; const APPLICATION_JSON = 'application/json'; describe('Content type', () => { diff --git a/src/plugins/console/public/lib/kb/__tests__/kb.test.js b/src/plugins/console/public/lib/kb/kb.test.js similarity index 96% rename from src/plugins/console/public/lib/kb/__tests__/kb.test.js rename to src/plugins/console/public/lib/kb/kb.test.js index eaf5023053880..a7e43f2e94a50 100644 --- a/src/plugins/console/public/lib/kb/__tests__/kb.test.js +++ b/src/plugins/console/public/lib/kb/kb.test.js @@ -18,11 +18,11 @@ */ import _ from 'lodash'; -import { populateContext } from '../../autocomplete/engine'; +import { populateContext } from '../autocomplete/engine'; -import '../../../application/models/sense_editor/sense_editor.test.mocks'; -import * as kb from '../../kb'; -import * as mappings from '../../mappings/mappings'; +import '../../application/models/sense_editor/sense_editor.test.mocks'; +import * as kb from '../kb'; +import * as mappings from '../mappings/mappings'; describe('Knowledge base', () => { beforeEach(() => { diff --git a/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js b/src/plugins/console/public/lib/mappings/mapping.test.js similarity index 98% rename from src/plugins/console/public/lib/mappings/__tests__/mapping.test.js rename to src/plugins/console/public/lib/mappings/mapping.test.js index ce52b060f418f..ab4c08fca1553 100644 --- a/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js +++ b/src/plugins/console/public/lib/mappings/mapping.test.js @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import '../../../application/models/sense_editor/sense_editor.test.mocks'; -import * as mappings from '../mappings'; +import '../../application/models/sense_editor/sense_editor.test.mocks'; +import * as mappings from './mappings'; describe('Mappings', () => { beforeEach(() => { diff --git a/src/plugins/console/public/lib/utils/__tests__/utils.test.js b/src/plugins/console/public/lib/utils/utils.test.js similarity index 99% rename from src/plugins/console/public/lib/utils/__tests__/utils.test.js rename to src/plugins/console/public/lib/utils/utils.test.js index e47e71c742a81..ee86756da8362 100644 --- a/src/plugins/console/public/lib/utils/__tests__/utils.test.js +++ b/src/plugins/console/public/lib/utils/utils.test.js @@ -17,7 +17,7 @@ * under the License. */ -import * as utils from '../'; +import * as utils from '.'; describe('Utils class', () => { test('extract deprecation messages', function () { diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts b/src/plugins/console/server/routes/api/console/proxy/body.test.ts similarity index 80% rename from src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts rename to src/plugins/console/server/routes/api/console/proxy/body.test.ts index d0c8383792796..b6ba08c13b06b 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/body.test.ts @@ -18,12 +18,11 @@ */ import { getProxyRouteHandlerDeps } from './mocks'; -import expect from '@kbn/expect'; import { Readable } from 'stream'; -import { kibanaResponseFactory } from '../../../../../../../../core/server'; -import { createHandler } from '../create_handler'; -import * as requestModule from '../../../../../lib/proxy_request'; +import { kibanaResponseFactory } from '../../../../../../../core/server'; +import { createHandler } from './create_handler'; +import * as requestModule from '../../../../lib/proxy_request'; import { createResponseStub } from './stubs'; describe('Console Proxy Route', () => { @@ -62,38 +61,38 @@ describe('Console Proxy Route', () => { describe('GET request', () => { it('returns the exact body', async () => { const { payload } = await request('GET', '/', 'foobar'); - expect(await readStream(payload)).to.be('foobar'); + expect(await readStream(payload)).toBe('foobar'); }); }); describe('POST request', () => { it('returns the exact body', async () => { const { payload } = await request('POST', '/', 'foobar'); - expect(await readStream(payload)).to.be('foobar'); + expect(await readStream(payload)).toBe('foobar'); }); }); describe('PUT request', () => { it('returns the exact body', async () => { const { payload } = await request('PUT', '/', 'foobar'); - expect(await readStream(payload)).to.be('foobar'); + expect(await readStream(payload)).toBe('foobar'); }); }); describe('DELETE request', () => { it('returns the exact body', async () => { const { payload } = await request('DELETE', '/', 'foobar'); - expect(await readStream(payload)).to.be('foobar'); + expect(await readStream(payload)).toBe('foobar'); }); }); describe('HEAD request', () => { it('returns the status code and text', async () => { const { payload } = await request('HEAD', '/'); - expect(typeof payload).to.be('string'); - expect(payload).to.be('200 - OK'); + expect(typeof payload).toBe('string'); + expect(payload).toBe('200 - OK'); }); describe('mixed casing', () => { it('returns the status code and text', async () => { const { payload } = await request('HeAd', '/'); - expect(typeof payload).to.be('string'); - expect(payload).to.be('200 - OK'); + expect(typeof payload).toBe('string'); + expect(payload).toBe('200 - OK'); }); }); }); diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/headers.test.ts b/src/plugins/console/server/routes/api/console/proxy/headers.test.ts similarity index 71% rename from src/plugins/console/server/routes/api/console/proxy/tests/headers.test.ts rename to src/plugins/console/server/routes/api/console/proxy/headers.test.ts index 2d4c616754e33..5ea08e7ada9ba 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/headers.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/headers.test.ts @@ -16,21 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -jest.mock('../../../../../../../../core/server/http/router/request', () => ({ +jest.mock('../../../../../../../core/server/http/router/request', () => ({ ensureRawRequest: jest.fn(), })); -import { kibanaResponseFactory } from '../../../../../../../../core/server'; +import { kibanaResponseFactory } from '../../../../../../../core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ensureRawRequest } from '../../../../../../../../core/server/http/router/request'; +import { ensureRawRequest } from '../../../../../../../core/server/http/router/request'; import { getProxyRouteHandlerDeps } from './mocks'; -import expect from '@kbn/expect'; -import * as requestModule from '../../../../../lib/proxy_request'; +import * as requestModule from '../../../../lib/proxy_request'; -import { createHandler } from '../create_handler'; +import { createHandler } from './create_handler'; import { createResponseStub } from './stubs'; @@ -74,16 +73,16 @@ describe('Console Proxy Route', () => { kibanaResponseFactory ); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); const [[{ headers }]] = (requestModule.proxyRequest as jest.Mock).mock.calls; - expect(headers).to.have.property('x-forwarded-for'); - expect(headers['x-forwarded-for']).to.be('0.0.0.0'); - expect(headers).to.have.property('x-forwarded-port'); - expect(headers['x-forwarded-port']).to.be('1234'); - expect(headers).to.have.property('x-forwarded-proto'); - expect(headers['x-forwarded-proto']).to.be('http'); - expect(headers).to.have.property('x-forwarded-host'); - expect(headers['x-forwarded-host']).to.be('test'); + expect(headers).toHaveProperty('x-forwarded-for'); + expect(headers['x-forwarded-for']).toBe('0.0.0.0'); + expect(headers).toHaveProperty('x-forwarded-port'); + expect(headers['x-forwarded-port']).toBe('1234'); + expect(headers).toHaveProperty('x-forwarded-proto'); + expect(headers['x-forwarded-proto']).toBe('http'); + expect(headers).toHaveProperty('x-forwarded-host'); + expect(headers['x-forwarded-host']).toBe('test'); }); }); }); diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts b/src/plugins/console/server/routes/api/console/proxy/mocks.ts similarity index 92% rename from src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts rename to src/plugins/console/server/routes/api/console/proxy/mocks.ts index 158a4a979683f..4d55a27d7aa2f 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts +++ b/src/plugins/console/server/routes/api/console/proxy/mocks.ts @@ -17,15 +17,15 @@ * under the License. */ -jest.mock('../../../../../lib/proxy_request', () => ({ +jest.mock('../../../../lib/proxy_request', () => ({ proxyRequest: jest.fn(), })); import { duration } from 'moment'; -import { ProxyConfigCollection } from '../../../../../lib'; -import { RouteDependencies, ProxyDependencies } from '../../../../../routes'; -import { EsLegacyConfigService, SpecDefinitionsService } from '../../../../../services'; -import { coreMock, httpServiceMock } from '../../../../../../../../core/server/mocks'; +import { ProxyConfigCollection } from '../../../../lib'; +import { RouteDependencies, ProxyDependencies } from '../../../../routes'; +import { EsLegacyConfigService, SpecDefinitionsService } from '../../../../services'; +import { coreMock, httpServiceMock } from '../../../../../../../core/server/mocks'; const defaultProxyValue = Object.freeze({ readLegacyESConfig: async () => ({ diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/params.test.ts b/src/plugins/console/server/routes/api/console/proxy/params.test.ts similarity index 87% rename from src/plugins/console/server/routes/api/console/proxy/tests/params.test.ts rename to src/plugins/console/server/routes/api/console/proxy/params.test.ts index fc1dae7fbcea2..8838fa405b88f 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/params.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/params.test.ts @@ -16,13 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { kibanaResponseFactory } from '../../../../../../../../core/server'; +import { kibanaResponseFactory } from '../../../../../../../core/server'; import { getProxyRouteHandlerDeps } from './mocks'; import { createResponseStub } from './stubs'; -import * as requestModule from '../../../../../lib/proxy_request'; -import expect from '@kbn/expect'; +import * as requestModule from '../../../../lib/proxy_request'; -import { createHandler } from '../create_handler'; +import { createHandler } from './create_handler'; describe('Console Proxy Route', () => { let handler: ReturnType; @@ -45,7 +44,7 @@ describe('Console Proxy Route', () => { kibanaResponseFactory ); - expect(status).to.be(403); + expect(status).toBe(403); }); }); describe('one match', () => { @@ -62,8 +61,8 @@ describe('Console Proxy Route', () => { kibanaResponseFactory ); - expect(status).to.be(200); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect(status).toBe(200); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); }); }); describe('all match', () => { @@ -80,8 +79,8 @@ describe('Console Proxy Route', () => { kibanaResponseFactory ); - expect(status).to.be(200); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect(status).toBe(200); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); }); }); }); diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts b/src/plugins/console/server/routes/api/console/proxy/proxy_fallback.test.ts similarity index 92% rename from src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts rename to src/plugins/console/server/routes/api/console/proxy/proxy_fallback.test.ts index 17ce715ac1afa..b9575b7abeea3 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/proxy_fallback.test.ts @@ -20,9 +20,9 @@ import { duration } from 'moment'; import { getProxyRouteHandlerDeps } from './mocks'; -import { kibanaResponseFactory } from '../../../../../../../../core/server'; -import * as requestModule from '../../../../../lib/proxy_request'; -import { createHandler } from '../create_handler'; +import { kibanaResponseFactory } from '../../../../../../../core/server'; +import * as requestModule from '../../../../lib/proxy_request'; +import { createHandler } from './create_handler'; describe('Console Proxy Route', () => { afterEach(async () => { diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts b/src/plugins/console/server/routes/api/console/proxy/query_string.test.ts similarity index 81% rename from src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts rename to src/plugins/console/server/routes/api/console/proxy/query_string.test.ts index f0e7e5d6e8f9a..7b7bd6b605d96 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/query_string.test.ts @@ -16,14 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { kibanaResponseFactory } from '../../../../../../../../core/server'; +import { kibanaResponseFactory } from '../../../../../../../core/server'; import { getProxyRouteHandlerDeps } from './mocks'; import { createResponseStub } from './stubs'; -import * as requestModule from '../../../../../lib/proxy_request'; +import * as requestModule from '../../../../lib/proxy_request'; -import expect from '@kbn/expect'; - -import { createHandler } from '../create_handler'; +import { createHandler } from './create_handler'; describe('Console Proxy Route', () => { let request: any; @@ -50,25 +48,25 @@ describe('Console Proxy Route', () => { describe('contains full url', () => { it('treats the url as a path', async () => { await request('GET', 'http://evil.com/test'); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); const [[args]] = (requestModule.proxyRequest as jest.Mock).mock.calls; - expect(args.uri.href).to.be('http://localhost:9200/http://evil.com/test?pretty=true'); + expect(args.uri.href).toBe('http://localhost:9200/http://evil.com/test?pretty=true'); }); }); describe('starts with a slash', () => { it('combines well with the base url', async () => { await request('GET', '/index/id'); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); const [[args]] = (requestModule.proxyRequest as jest.Mock).mock.calls; - expect(args.uri.href).to.be('http://localhost:9200/index/id?pretty=true'); + expect(args.uri.href).toBe('http://localhost:9200/index/id?pretty=true'); }); }); describe(`doesn't start with a slash`, () => { it('combines well with the base url', async () => { await request('GET', 'index/id'); - expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1); + expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1); const [[args]] = (requestModule.proxyRequest as jest.Mock).mock.calls; - expect(args.uri.href).to.be('http://localhost:9200/index/id?pretty=true'); + expect(args.uri.href).toBe('http://localhost:9200/index/id?pretty=true'); }); }); }); diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/route_validation.test.ts b/src/plugins/console/server/routes/api/console/proxy/route_validation.test.ts similarity index 96% rename from src/plugins/console/server/routes/api/console/proxy/tests/route_validation.test.ts rename to src/plugins/console/server/routes/api/console/proxy/route_validation.test.ts index 2588c96e3b091..a67c742f09fb5 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/route_validation.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/route_validation.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { routeValidationConfig } from '../validation_config'; +import { routeValidationConfig } from './validation_config'; const { query } = routeValidationConfig; diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/stubs.ts b/src/plugins/console/server/routes/api/console/proxy/stubs.ts similarity index 100% rename from src/plugins/console/server/routes/api/console/proxy/tests/stubs.ts rename to src/plugins/console/server/routes/api/console/proxy/stubs.ts diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index 1ea6355b9c558..baa7ffc5b8de3 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -41,6 +41,7 @@ import { PluginInitializerContext, ScopedHistory, } from '../services/core'; +import { DashboardNoMatch } from './listing/dashboard_no_match'; export const dashboardUrlParams = { showTopMenu: 'show-top-menu', @@ -77,6 +78,7 @@ export async function mountApp({ const { navigation, savedObjects, + urlForwarding, data: dataStart, share: shareStart, embeddable: embeddableStart, @@ -88,6 +90,7 @@ export async function mountApp({ navigation, onAppLeave, savedObjects, + urlForwarding, usageCollection, core: coreStart, data: dataStart, @@ -180,6 +183,10 @@ export async function mountApp({ ); }; + const renderNoMatch = (routeProps: RouteComponentProps) => { + return ; + }; + // make sure the index pattern list is up to date await dataStart.indexPatterns.clearCache(); @@ -202,9 +209,10 @@ export async function mountApp({ render={renderDashboard} /> - + + diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx index 3aee05554b0d9..8172be46e9f3a 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx @@ -39,6 +39,7 @@ import { dataPluginMock } from '../../../../data/public/mocks'; import { chromeServiceMock, coreMock } from '../../../../../core/public/mocks'; import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; +import { UrlForwardingStart } from '../../../../url_forwarding/public'; function makeDefaultServices(): DashboardAppServices { const core = coreMock.createStart(); @@ -71,6 +72,7 @@ function makeDefaultServices(): DashboardAppServices { scopedHistory: () => ({} as ScopedHistory), savedQueryService: {} as SavedQueryService, setHeaderActionMenu: (mountPoint) => {}, + urlForwarding: {} as UrlForwardingStart, uiSettings: {} as IUiSettingsClient, restorePreviousUrl: () => {}, onAppLeave: (handler) => {}, diff --git a/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx b/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx new file mode 100644 index 0000000000000..a0f13af92ff77 --- /dev/null +++ b/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCallOut } from '@elastic/eui'; + +import { RouteComponentProps } from 'react-router-dom'; +import { useKibana, toMountPoint } from '../../services/kibana_react'; +import { DashboardAppServices } from '../types'; +import { DashboardConstants } from '../..'; + +let bannerId: string | undefined; + +export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['history'] }) => { + const { services } = useKibana(); + + useEffect(() => { + services.restorePreviousUrl(); + + const { navigated } = services.urlForwarding.navigateToLegacyKibanaUrl( + history.location.pathname + ); + + if (!navigated) { + const bannerMessage = i18n.translate('dashboard.noMatchRoute.bannerTitleText', { + defaultMessage: 'Page not found', + }); + + bannerId = services.core.overlays.banners.replace( + bannerId, + toMountPoint( + +

+ +

+
+ ) + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + setTimeout(() => { + if (bannerId) { + services.core.overlays.banners.remove(bannerId); + } + }, 15000); + + history.replace(DashboardConstants.LANDING_PAGE_PATH); + } + }, [services, history]); + + return null; +}; diff --git a/src/plugins/dashboard/public/application/types.ts b/src/plugins/dashboard/public/application/types.ts index d1caaa349d80b..75620fd73360d 100644 --- a/src/plugins/dashboard/public/application/types.ts +++ b/src/plugins/dashboard/public/application/types.ts @@ -33,6 +33,7 @@ import { NavigationPublicPluginStart } from '../services/navigation'; import { SavedObjectsTaggingApi } from '../services/saved_objects_tagging_oss'; import { DataPublicPluginStart, IndexPatternsContract } from '../services/data'; import { SavedObjectLoader, SavedObjectsStart } from '../services/saved_objects'; +import { UrlForwardingStart } from '../../../url_forwarding/public'; export type DashboardRedirect = (props: RedirectToProps) => void; export type RedirectToProps = @@ -75,6 +76,7 @@ export interface DashboardAppServices { uiSettings: IUiSettingsClient; restorePreviousUrl: () => void; savedObjects: SavedObjectsStart; + urlForwarding: UrlForwardingStart; savedDashboards: SavedObjectLoader; scopedHistory: () => ScopedHistory; indexPatterns: IndexPatternsContract; diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx index 13bb8443ffef6..2448d5f22ced2 100644 --- a/src/plugins/data/README.mdx +++ b/src/plugins/data/README.mdx @@ -1,6 +1,6 @@ --- id: kibDataPlugin -slug: /kibana-dev-guide/services/data-plugin +slug: /kibana-dev-docs/services/data-plugin title: Data services image: https://source.unsplash.com/400x175/?Search summary: The data plugin contains services for searching, querying and filtering. diff --git a/src/plugins/region_map/public/get_deprecation_message.tsx b/src/plugins/region_map/public/get_deprecation_message.tsx index ea5cdf42c3111..de094fa98750f 100644 --- a/src/plugins/region_map/public/get_deprecation_message.tsx +++ b/src/plugins/region_map/public/get_deprecation_message.tsx @@ -71,6 +71,7 @@ export function getDeprecationMessage(vis: Vis) { const bucketAggs = vis.data?.aggs?.byType('buckets'); if (bucketAggs?.length && bucketAggs[0].type.dslName === 'terms') { createUrlParams.termsFieldName = bucketAggs[0].getField()?.name; + createUrlParams.termsSize = bucketAggs[0].getParam('size'); } const metricAggs = vis.data?.aggs?.byType('metrics'); diff --git a/test/functional/apps/dashboard/legacy_urls.ts b/test/functional/apps/dashboard/legacy_urls.ts index 6bb8d808e8daa..2a30bbf5e1f0a 100644 --- a/test/functional/apps/dashboard/legacy_urls.ts +++ b/test/functional/apps/dashboard/legacy_urls.ts @@ -92,6 +92,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visEditor.setMarkdownTxt(`[abc](#/dashboard/${testDashboardId})`); await PageObjects.visEditor.clickGo(); + + await PageObjects.visualize.saveVisualizationExpectSuccess('legacy url markdown'); + (await find.byLinkText('abc')).click(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -109,6 +112,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.expectMarkdownTextArea(); await browser.goForward(); }); + + it('resolves markdown link from dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.addVisualization('legacy url markdown'); + (await find.byLinkText('abc')).click(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.timePicker.setDefaultDataRange(); + + await PageObjects.dashboard.waitForRenderComplete(); + await pieChart.expectPieSliceCount(5); + }); }); }); } diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts index 8e4ea4faf014c..721b8cb82f65f 100644 --- a/x-pack/examples/alerting_example/common/constants.ts +++ b/x-pack/examples/alerting_example/common/constants.ts @@ -10,15 +10,16 @@ export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; // always firing export const DEFAULT_INSTANCES_TO_GENERATE = 5; +export interface AlwaysFiringThresholds { + small?: number; + medium?: number; + large?: number; +} export interface AlwaysFiringParams extends AlertTypeParams { instances?: number; - thresholds?: { - small?: number; - medium?: number; - large?: number; - }; + thresholds?: AlwaysFiringThresholds; } -export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds']; +export type AlwaysFiringActionGroupIds = keyof AlwaysFiringThresholds; // Astros export enum Craft { diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index cee7ee62e3210..42ce6df6d1a6f 100644 --- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -133,8 +133,10 @@ export const AlwaysFiringExpression: React.FunctionComponent< }; interface TShirtSelectorProps { - actionGroup?: ActionGroupWithCondition; - setTShirtThreshold: (actionGroup: ActionGroupWithCondition) => void; + actionGroup?: ActionGroupWithCondition; + setTShirtThreshold: ( + actionGroup: ActionGroupWithCondition + ) => void; } const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => { const [isOpen, setIsOpen] = useState(false); diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index 4fde4183b414e..fc837fee08b6f 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -11,6 +11,7 @@ import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID, AlwaysFiringParams, + AlwaysFiringActionGroupIds, } from '../../common/constants'; type ActionGroups = 'small' | 'medium' | 'large'; @@ -39,7 +40,8 @@ export const alertType: AlertType< AlwaysFiringParams, { count?: number }, { triggerdOnCycle: number }, - never + never, + AlwaysFiringActionGroupIds > = { id: 'example.always-firing', name: 'Always firing', diff --git a/x-pack/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts index 22c2f25c410cd..938baa8b317ba 100644 --- a/x-pack/examples/alerting_example/server/alert_types/astros.ts +++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts @@ -41,7 +41,10 @@ function getCraftFilter(craft: string) { export const alertType: AlertType< { outerSpaceCapacity: number; craft: string; op: string }, { peopleInSpace: number }, - { craft: string } + { craft: string }, + never, + 'default', + 'hasLandedBackOnEarth' > = { id: 'example.people-in-space', name: 'People In Space Right Now', diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index cf4ace99ed5dc..4afbbb3a33615 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -139,10 +139,11 @@ interface GetActionTypeParams { } // action type definition +export const ActionTypeId = '.email'; export function getActionType(params: GetActionTypeParams): EmailActionType { const { logger, publicBaseUrl, configurationUtilities } = params; return { - id: '.email', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.emailTitle', { defaultMessage: 'Email', diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 6926c826f776e..1b739b1567c6d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -39,11 +39,11 @@ const ParamsSchema = schema.object({ documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), }); -export const ES_INDEX_ACTION_TYPE_ID = '.index'; +export const ActionTypeId = '.index'; // action type definition export function getActionType({ logger }: { logger: Logger }): ESIndexActionType { return { - id: ES_INDEX_ACTION_TYPE_ID, + id: ActionTypeId, minimumLicenseRequired: 'basic', name: i18n.translate('xpack.actions.builtin.esIndexTitle', { defaultMessage: 'Index', diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index c2058d63683bf..3a01b875ec4a0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -18,6 +18,34 @@ import { getActionType as getServiceNowActionType } from './servicenow'; import { getActionType as getJiraActionType } from './jira'; import { getActionType as getResilientActionType } from './resilient'; import { getActionType as getTeamsActionType } from './teams'; +export { ActionParamsType as EmailActionParams, ActionTypeId as EmailActionTypeId } from './email'; +export { + ActionParamsType as IndexActionParams, + ActionTypeId as IndexActionTypeId, +} from './es_index'; +export { + ActionParamsType as PagerDutyActionParams, + ActionTypeId as PagerDutyActionTypeId, +} from './pagerduty'; +export { + ActionParamsType as ServerLogActionParams, + ActionTypeId as ServerLogActionTypeId, +} from './server_log'; +export { ActionParamsType as SlackActionParams, ActionTypeId as SlackActionTypeId } from './slack'; +export { + ActionParamsType as WebhookActionParams, + ActionTypeId as WebhookActionTypeId, +} from './webhook'; +export { + ActionParamsType as ServiceNowActionParams, + ActionTypeId as ServiceNowActionTypeId, +} from './servicenow'; +export { ActionParamsType as JiraActionParams, ActionTypeId as JiraActionTypeId } from './jira'; +export { + ActionParamsType as ResilientActionParams, + ActionTypeId as ResilientActionTypeId, +} from './resilient'; +export { ActionParamsType as TeamsActionParams, ActionTypeId as TeamsActionTypeId } from './teams'; export function registerBuiltInActionTypes({ actionsConfigUtils: configurationUtilities, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 4518fa0f119d5..d701fad0e0c2f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -5,7 +5,7 @@ */ import { curry } from 'lodash'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { validate } from './validators'; import { @@ -32,6 +32,7 @@ import { import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; +export type ActionParamsType = TypeOf; interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; @@ -47,6 +48,7 @@ const supportedSubActions: string[] = [ 'issue', ]; +export const ActionTypeId = '.jira'; // action type definition export function getActionType( params: GetActionTypeParams @@ -58,7 +60,7 @@ export function getActionType( > { const { logger, configurationUtilities } = params; return { - id: '.jira', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.NAME, validate: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 4574b748e6014..ccd25da2397bb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -117,6 +117,7 @@ function validateParams(paramsObject: unknown): string | void { } } +export const ActionTypeId = '.pagerduty'; // action type definition export function getActionType({ logger, @@ -126,7 +127,7 @@ export function getActionType({ configurationUtilities: ActionsConfigurationUtilities; }): PagerDutyActionType { return { - id: '.pagerduty', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.pagerdutyTitle', { defaultMessage: 'PagerDuty', diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts index 7ce9369289554..fca99f81d62bd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts @@ -5,7 +5,7 @@ */ import { curry } from 'lodash'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { validate } from './validators'; import { @@ -30,6 +30,8 @@ import { import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; +export type ActionParamsType = TypeOf; + interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; @@ -37,6 +39,7 @@ interface GetActionTypeParams { const supportedSubActions: string[] = ['getFields', 'pushToService', 'incidentTypes', 'severity']; +export const ActionTypeId = '.resilient'; // action type definition export function getActionType( params: GetActionTypeParams @@ -48,7 +51,7 @@ export function getActionType( > { const { logger, configurationUtilities } = params; return { - id: '.resilient', + id: ActionTypeId, minimumLicenseRequired: 'platinum', name: i18n.NAME, validate: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts index c485de8628f14..4cfea6aa9d889 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts @@ -38,11 +38,11 @@ const ParamsSchema = schema.object({ ), }); -export const SERVER_LOG_ACTION_TYPE_ID = '.server-log'; +export const ActionTypeId = '.server-log'; // action type definition export function getActionType({ logger }: { logger: Logger }): ServerLogActionType { return { - id: SERVER_LOG_ACTION_TYPE_ID, + id: ActionTypeId, minimumLicenseRequired: 'basic', name: i18n.translate('xpack.actions.builtin.serverLogTitle', { defaultMessage: 'Server log', diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 3fa8b25b86e8b..1f75d439200e3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -5,7 +5,7 @@ */ import { curry } from 'lodash'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { validate } from './validators'; import { @@ -29,11 +29,14 @@ import { ServiceNowExecutorResultData, } from './types'; +export type ActionParamsType = TypeOf; + interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; } +export const ActionTypeId = '.servicenow'; // action type definition export function getActionType( params: GetActionTypeParams @@ -45,7 +48,7 @@ export function getActionType( > { const { logger, configurationUtilities } = params; return { - id: '.servicenow', + id: ActionTypeId, minimumLicenseRequired: 'platinum', name: i18n.NAME, validate: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index a9155c329c175..c9a3c39afd049 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -52,6 +52,7 @@ const ParamsSchema = schema.object({ // action type definition +export const ActionTypeId = '.slack'; // customizing executor is only used for tests export function getActionType({ logger, @@ -63,7 +64,7 @@ export function getActionType({ executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; }): SlackActionType { return { - id: '.slack', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.slackTitle', { defaultMessage: 'Slack', diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.ts index e152a65217ce2..8575ae75d1e6c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.ts @@ -42,6 +42,7 @@ const ParamsSchema = schema.object({ message: schema.string({ minLength: 1 }), }); +export const ActionTypeId = '.teams'; // action type definition export function getActionType({ logger, @@ -51,7 +52,7 @@ export function getActionType({ configurationUtilities: ActionsConfigurationUtilities; }): TeamsActionType { return { - id: '.teams', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.teamsTitle', { defaultMessage: 'Microsoft Teams', diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 089363990643f..4479f7c69bebb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -71,6 +71,7 @@ const ParamsSchema = schema.object({ body: schema.maybe(schema.string()), }); +export const ActionTypeId = '.webhook'; // action type definition export function getActionType({ logger, @@ -80,7 +81,7 @@ export function getActionType({ configurationUtilities: ActionsConfigurationUtilities; }): WebhookActionType { return { - id: '.webhook', + id: ActionTypeId, minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.webhookTitle', { defaultMessage: 'Webhook', diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 39bfe2c2820e2..c43cc20bd4773 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -21,6 +21,30 @@ export { ActionType, PreConfiguredAction, } from './types'; + +export type { + EmailActionTypeId, + EmailActionParams, + IndexActionTypeId, + IndexActionParams, + PagerDutyActionTypeId, + PagerDutyActionParams, + ServerLogActionTypeId, + ServerLogActionParams, + SlackActionTypeId, + SlackActionParams, + WebhookActionTypeId, + WebhookActionParams, + ServiceNowActionTypeId, + ServiceNowActionParams, + JiraActionTypeId, + JiraActionParams, + ResilientActionTypeId, + ResilientActionParams, + TeamsActionTypeId, + TeamsActionParams, +} from './builtin_action_types'; + export { PluginSetupContract, PluginStartContract } from './plugin'; export { asSavedObjectExecutionSource, asHttpRequestExecutionSource } from './lib'; diff --git a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts index 0f309bb76b76c..f22e87a58ec7f 100644 --- a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts +++ b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts @@ -5,14 +5,13 @@ */ import { ActionType } from '../types'; import { LICENSE_TYPE } from '../../../licensing/common/types'; -import { SERVER_LOG_ACTION_TYPE_ID } from '../builtin_action_types/server_log'; -import { ES_INDEX_ACTION_TYPE_ID } from '../builtin_action_types/es_index'; +import { ServerLogActionTypeId, IndexActionTypeId } from '../builtin_action_types'; import { CASE_ACTION_TYPE_ID } from '../../../case/server'; import { ActionTypeConfig, ActionTypeSecrets, ActionTypeParams } from '../types'; const ACTIONS_SCOPED_WITHIN_STACK = new Set([ - SERVER_LOG_ACTION_TYPE_ID, - ES_INDEX_ACTION_TYPE_ID, + ServerLogActionTypeId, + IndexActionTypeId, CASE_ACTION_TYPE_ID, ]); diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 39dc23c7bbb73..2191b23eec11e 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -142,8 +142,41 @@ This example receives server and threshold as parameters. It will read the CPU u ```typescript import { schema } from '@kbn/config-schema'; +import { + Alert, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext +} from 'x-pack/plugins/alerts/common'; ... -server.newPlatform.setup.plugins.alerts.registerType({ +interface MyAlertTypeParams extends AlertTypeParams { + server: string; + threshold: number; +} + +interface MyAlertTypeState extends AlertTypeState { + lastChecked: number; +} + +interface MyAlertTypeInstanceState extends AlertInstanceState { + cpuUsage: number; +} + +interface MyAlertTypeInstanceContext extends AlertInstanceContext { + server: string; + hasCpuUsageIncreased: boolean; +} + +type MyAlertTypeActionGroups = 'default' | 'warning'; + +const myAlertType: AlertType< + MyAlertTypeParams, + MyAlertTypeState, + MyAlertTypeInstanceState, + MyAlertTypeInstanceContext, + MyAlertTypeActionGroups +> = { id: 'my-alert-type', name: 'My alert type', validate: { @@ -180,7 +213,7 @@ server.newPlatform.setup.plugins.alerts.registerType({ services, params, state, - }: AlertExecutorOptions) { + }: AlertExecutorOptions) { // Let's assume params is { server: 'server_1', threshold: 0.8 } const { server, threshold } = params; @@ -219,7 +252,9 @@ server.newPlatform.setup.plugins.alerts.registerType({ }; }, producer: 'alerting', -}); +}; + +server.newPlatform.setup.plugins.alerts.registerType(myAlertType); ``` This example only receives threshold as a parameter. It will read the CPU usage of all the servers and schedule individual actions if the reading for a server is greater than the threshold. This is a better implementation than above as only one query is performed for all the servers instead of one query per server. diff --git a/x-pack/plugins/alerts/common/alert_type.ts b/x-pack/plugins/alerts/common/alert_type.ts index 4ab3ddc7ca810..d10d2467516cc 100644 --- a/x-pack/plugins/alerts/common/alert_type.ts +++ b/x-pack/plugins/alerts/common/alert_type.ts @@ -5,19 +5,29 @@ */ import { LicenseType } from '../../licensing/common/types'; +import { RecoveredActionGroupId, DefaultActionGroupId } from './builtin_action_groups'; -export interface AlertType { +export interface AlertType< + ActionGroupIds extends Exclude = DefaultActionGroupId, + RecoveryActionGroupId extends string = RecoveredActionGroupId +> { id: string; name: string; - actionGroups: ActionGroup[]; - recoveryActionGroup: ActionGroup; + actionGroups: Array>; + recoveryActionGroup: ActionGroup; actionVariables: string[]; - defaultActionGroupId: ActionGroup['id']; + defaultActionGroupId: ActionGroupIds; producer: string; minimumLicenseRequired: LicenseType; } -export interface ActionGroup { - id: string; +export interface ActionGroup { + id: ActionGroupIds; name: string; } + +export type ActionGroupIdsOf = T extends ActionGroup + ? groups + : T extends Readonly> + ? groups + : never; diff --git a/x-pack/plugins/alerts/common/builtin_action_groups.ts b/x-pack/plugins/alerts/common/builtin_action_groups.ts index e23bbcc54b24d..f2b7ec855b86e 100644 --- a/x-pack/plugins/alerts/common/builtin_action_groups.ts +++ b/x-pack/plugins/alerts/common/builtin_action_groups.ts @@ -6,13 +6,27 @@ import { i18n } from '@kbn/i18n'; import { ActionGroup } from './alert_type'; -export const RecoveredActionGroup: Readonly = { +export type DefaultActionGroupId = 'default'; + +export type RecoveredActionGroupId = typeof RecoveredActionGroup['id']; +export const RecoveredActionGroup: Readonly> = Object.freeze({ id: 'recovered', name: i18n.translate('xpack.alerts.builtinActionGroups.recovered', { defaultMessage: 'Recovered', }), -}; +}); + +export type ReservedActionGroups = + | RecoveryActionGroupId + | RecoveredActionGroupId; + +export type WithoutReservedActionGroups< + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> = ActionGroupIds extends ReservedActionGroups ? never : ActionGroupIds; -export function getBuiltinActionGroups(customRecoveryGroup?: ActionGroup): ActionGroup[] { - return [customRecoveryGroup ?? Object.freeze(RecoveredActionGroup)]; +export function getBuiltinActionGroups( + customRecoveryGroup?: ActionGroup +): [ActionGroup>] { + return [customRecoveryGroup ?? RecoveredActionGroup]; } diff --git a/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts index b428f6c1a9134..1bd08fc3ac32d 100644 --- a/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts +++ b/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts @@ -6,6 +6,7 @@ import sinon from 'sinon'; import { AlertInstance } from './alert_instance'; +import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; let clock: sinon.SinonFakeTimers; @@ -17,12 +18,20 @@ afterAll(() => clock.restore()); describe('hasScheduledActions()', () => { test('defaults to false', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); expect(alertInstance.hasScheduledActions()).toEqual(false); }); test('returns true when scheduleActions is called', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.hasScheduledActions()).toEqual(true); }); @@ -30,7 +39,11 @@ describe('hasScheduledActions()', () => { describe('isThrottled', () => { test(`should throttle when group didn't change and throttle period is still active`, () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -44,7 +57,11 @@ describe('isThrottled', () => { }); test(`shouldn't throttle when group didn't change and throttle period expired`, () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -58,7 +75,7 @@ describe('isThrottled', () => { }); test(`shouldn't throttle when group changes`, () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance({ meta: { lastScheduledActions: { date: new Date(), @@ -74,12 +91,20 @@ describe('isThrottled', () => { describe('scheduledActionGroupOrSubgroupHasChanged()', () => { test('should be false if no last scheduled and nothing scheduled', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); }); test('should be false if group does not change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -92,7 +117,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group and subgroup does not change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -106,7 +135,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group does not change and subgroup goes from undefined to defined', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -119,7 +152,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group does not change and subgroup goes from defined to undefined', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -133,13 +170,17 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if no last scheduled and has scheduled action', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); }); test('should be true if group does change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance({ meta: { lastScheduledActions: { date: new Date(), @@ -152,7 +193,7 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if group does change and subgroup does change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance({ meta: { lastScheduledActions: { date: new Date(), @@ -166,7 +207,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if group does not change and subgroup does change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -182,14 +227,22 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { describe('getScheduledActionOptions()', () => { test('defaults to undefined', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); expect(alertInstance.getScheduledActionOptions()).toBeUndefined(); }); }); describe('unscheduleActions()', () => { test('makes hasScheduledActions() return false', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.hasScheduledActions()).toEqual(true); alertInstance.unscheduleActions(); @@ -197,7 +250,11 @@ describe('unscheduleActions()', () => { }); test('makes getScheduledActionOptions() return undefined', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.getScheduledActionOptions()).toEqual({ actionGroup: 'default', @@ -212,14 +269,22 @@ describe('unscheduleActions()', () => { describe('getState()', () => { test('returns state passed to constructor', () => { const state = { foo: true }; - const alertInstance = new AlertInstance({ state }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state }); expect(alertInstance.getState()).toEqual(state); }); }); describe('scheduleActions()', () => { test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -233,7 +298,11 @@ describe('scheduleActions()', () => { }); test('makes isThrottled() return true when throttled', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -247,7 +316,11 @@ describe('scheduleActions()', () => { }); test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -262,7 +335,11 @@ describe('scheduleActions()', () => { }); test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance({ state: { foo: true }, meta: {} }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: {} }); alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); expect(alertInstance.getScheduledActionOptions()).toEqual({ actionGroup: 'default', @@ -272,7 +349,11 @@ describe('scheduleActions()', () => { }); test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default', { field: true }); expect(() => alertInstance.scheduleActions('default', { field: false }) @@ -284,7 +365,11 @@ describe('scheduleActions()', () => { describe('scheduleActionsWithSubGroup()', () => { test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -300,7 +385,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return true when throttled and subgroup is the same', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -317,7 +406,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -333,7 +426,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return false when throttled and subgroup is the different', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -350,7 +447,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -367,7 +468,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance({ state: { foo: true }, meta: {} }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: {} }); alertInstance .replaceState({ otherField: true }) .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); @@ -380,7 +485,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); expect(() => alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -390,7 +499,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice with different subgroups', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); expect(() => alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -400,7 +513,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice whether there are subgroups', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default', { field: true }); expect(() => alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -412,7 +529,11 @@ describe('scheduleActionsWithSubGroup()', () => { describe('replaceState()', () => { test('replaces previous state', () => { - const alertInstance = new AlertInstance({ state: { foo: true } }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true } }); alertInstance.replaceState({ bar: true }); expect(alertInstance.getState()).toEqual({ bar: true }); alertInstance.replaceState({ baz: true }); @@ -422,7 +543,11 @@ describe('replaceState()', () => { describe('updateLastScheduledActions()', () => { test('replaces previous lastScheduledActions', () => { - const alertInstance = new AlertInstance({ meta: {} }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: {} }); alertInstance.updateLastScheduledActions('default'); expect(alertInstance.toJSON()).toEqual({ state: {}, @@ -438,7 +563,11 @@ describe('updateLastScheduledActions()', () => { describe('toJSON', () => { test('only serializes state and meta', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -464,7 +593,11 @@ describe('toRaw', () => { }, }, }; - const alertInstance = new AlertInstance(raw); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(raw); expect(alertInstance.toRaw()).toEqual(raw); }); }); diff --git a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts index 8841f3115d547..c49b38e157a07 100644 --- a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts +++ b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts @@ -9,15 +9,17 @@ import { RawAlertInstance, rawAlertInstance, AlertInstanceContext, + DefaultActionGroupId, } from '../../common'; import { parseDuration } from '../lib'; interface ScheduledExecutionOptions< State extends AlertInstanceState, - Context extends AlertInstanceContext + Context extends AlertInstanceContext, + ActionGroupIds extends string = DefaultActionGroupId > { - actionGroup: string; + actionGroup: ActionGroupIds; subgroup?: string; context: Context; state: State; @@ -25,17 +27,19 @@ interface ScheduledExecutionOptions< export type PublicAlertInstance< State extends AlertInstanceState = AlertInstanceState, - Context extends AlertInstanceContext = AlertInstanceContext + Context extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = DefaultActionGroupId > = Pick< - AlertInstance, + AlertInstance, 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' >; export class AlertInstance< State extends AlertInstanceState = AlertInstanceState, - Context extends AlertInstanceContext = AlertInstanceContext + Context extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never > { - private scheduledExecutionOptions?: ScheduledExecutionOptions; + private scheduledExecutionOptions?: ScheduledExecutionOptions; private meta: AlertInstanceMeta; private state: State; @@ -97,14 +101,14 @@ export class AlertInstance< private scheduledActionGroupIsUnchanged( lastScheduledActions: NonNullable, - scheduledExecutionOptions: ScheduledExecutionOptions + scheduledExecutionOptions: ScheduledExecutionOptions ) { return lastScheduledActions.group === scheduledExecutionOptions.actionGroup; } private scheduledActionSubgroupIsUnchanged( lastScheduledActions: NonNullable, - scheduledExecutionOptions: ScheduledExecutionOptions + scheduledExecutionOptions: ScheduledExecutionOptions ) { return lastScheduledActions.subgroup && scheduledExecutionOptions.subgroup ? lastScheduledActions.subgroup === scheduledExecutionOptions.subgroup @@ -128,7 +132,7 @@ export class AlertInstance< return this.state; } - scheduleActions(actionGroup: string, context: Context = {} as Context) { + scheduleActions(actionGroup: ActionGroupIds, context: Context = {} as Context) { this.ensureHasNoScheduledActions(); this.scheduledExecutionOptions = { actionGroup, @@ -139,7 +143,7 @@ export class AlertInstance< } scheduleActionsWithSubGroup( - actionGroup: string, + actionGroup: ActionGroupIds, subgroup: string, context: Context = {} as Context ) { @@ -164,7 +168,7 @@ export class AlertInstance< return this; } - updateLastScheduledActions(group: string, subgroup?: string) { + updateLastScheduledActions(group: ActionGroupIds, subgroup?: string) { this.meta.lastScheduledActions = { group, subgroup, date: new Date() }; } diff --git a/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts index 47f013a5d0e55..6ba4a8b57d9de 100644 --- a/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts +++ b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts @@ -9,11 +9,12 @@ import { AlertInstance } from './alert_instance'; export function createAlertInstanceFactory< InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext ->(alertInstances: Record>) { - return (id: string): AlertInstance => { + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string +>(alertInstances: Record>) { + return (id: string): AlertInstance => { if (!alertInstances[id]) { - alertInstances[id] = new AlertInstance(); + alertInstances[id] = new AlertInstance(); } return alertInstances[id]; diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index 58b2cb74f2353..1fdd64d56d466 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -6,7 +6,7 @@ import { TaskRunnerFactory } from './task_runner'; import { AlertTypeRegistry, ConstructorOptions } from './alert_type_registry'; -import { AlertType } from './types'; +import { ActionGroup, AlertType } from './types'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { ILicenseState } from './lib/license_state'; import { licenseStateMock } from './lib/license_state.mock'; @@ -55,7 +55,7 @@ describe('has()', () => { describe('register()', () => { test('throws if AlertType Id contains invalid characters', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -87,7 +87,7 @@ describe('register()', () => { }); test('throws if AlertType Id isnt a string', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: (123 as unknown) as string, name: 'Test', actionGroups: [ @@ -109,7 +109,7 @@ describe('register()', () => { }); test('throws if AlertType action groups contains reserved group id', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -117,10 +117,14 @@ describe('register()', () => { id: 'default', name: 'Default', }, - { + /** + * The type system will ensure you can't use the `recovered` action group + * but we also want to ensure this at runtime + */ + ({ id: 'recovered', name: 'Recovered', - }, + } as unknown) as ActionGroup<'NotReserved'>, ], defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -137,7 +141,7 @@ describe('register()', () => { }); test('allows an AlertType to specify a custom recovery group', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -172,7 +176,14 @@ describe('register()', () => { }); test('throws if the custom recovery group is contained in the AlertType action groups', () => { - const alertType: AlertType = { + const alertType: AlertType< + never, + never, + never, + never, + 'default' | 'backToAwesome', + 'backToAwesome' + > = { id: 'test', name: 'Test', actionGroups: [ @@ -204,7 +215,7 @@ describe('register()', () => { }); test('registers the executor with the task manager', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -234,7 +245,7 @@ describe('register()', () => { }); test('shallow clones the given alert type', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -475,8 +486,12 @@ describe('ensureAlertTypeEnabled', () => { }); }); -function alertTypeWithVariables(id: string, context: string, state: string): AlertType { - const baseAlert: AlertType = { +function alertTypeWithVariables( + id: ActionGroupIds, + context: string, + state: string +): AlertType { + const baseAlert: AlertType = { id, name: `${id}-name`, actionGroups: [], diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts index 5e4188c1f3bc1..c26088b6bce3c 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.ts @@ -19,7 +19,12 @@ import { AlertInstanceState, AlertInstanceContext, } from './types'; -import { RecoveredActionGroup, getBuiltinActionGroups } from '../common'; +import { + RecoveredActionGroup, + getBuiltinActionGroups, + RecoveredActionGroupId, + ActionGroup, +} from '../common'; import { ILicenseState } from './lib/license_state'; import { getAlertTypeFeatureUsageName } from './lib/get_alert_type_feature_usage_name'; @@ -69,15 +74,36 @@ export type NormalizedAlertType< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext -> = Omit, 'recoveryActionGroup'> & - Pick>, 'recoveryActionGroup'>; + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> = { + actionGroups: Array>; +} & Omit< + AlertType, + 'recoveryActionGroup' | 'actionGroups' +> & + Pick< + Required< + AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + >, + 'recoveryActionGroup' + >; export type UntypedNormalizedAlertType = NormalizedAlertType< AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string, + string >; export class AlertTypeRegistry { @@ -106,8 +132,19 @@ export class AlertTypeRegistry { Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext - >(alertType: AlertType) { + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string + >( + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + ) { if (this.has(alertType.id)) { throw new Error( i18n.translate('xpack.alerts.alertTypeRegistry.register.duplicateAlertTypeError', { @@ -124,18 +161,28 @@ export class AlertTypeRegistry { Params, State, InstanceState, - InstanceContext + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId >(alertType); this.alertTypes.set( alertIdSchema.validate(alertType.id), - normalizedAlertType as UntypedNormalizedAlertType + /** stripping the typing is required in order to store the AlertTypes in a Map */ + (normalizedAlertType as unknown) as UntypedNormalizedAlertType ); this.taskManager.registerTaskDefinitions({ [`alerting:${alertType.id}`]: { title: alertType.name, createTaskRunner: (context: RunContext) => - this.taskRunnerFactory.create(normalizedAlertType as UntypedNormalizedAlertType, context), + this.taskRunnerFactory.create< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId | RecoveredActionGroupId + >(normalizedAlertType, context), }, }); // No need to notify usage on basic alert types @@ -151,8 +198,19 @@ export class AlertTypeRegistry { Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext - >(id: string): NormalizedAlertType { + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = string, + RecoveryActionGroupId extends string = string + >( + id: string + ): NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > { if (!this.has(id)) { throw Boom.badRequest( i18n.translate('xpack.alerts.alertTypeRegistry.get.missingAlertTypeError', { @@ -163,11 +221,18 @@ export class AlertTypeRegistry { }) ); } - return this.alertTypes.get(id)! as NormalizedAlertType< + /** + * When we store the AlertTypes in the Map we strip the typing. + * This means that returning a typed AlertType in `get` is an inherently + * unsafe operation. Down casting to `unknown` is the only way to achieve this. + */ + return (this.alertTypes.get(id)! as unknown) as NormalizedAlertType< Params, State, InstanceState, - InstanceContext + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId >; } @@ -217,15 +282,31 @@ function augmentActionGroupsWithReserved< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string >( - alertType: AlertType -): NormalizedAlertType { + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > +): NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveredActionGroupId | RecoveryActionGroupId +> { const reservedActionGroups = getBuiltinActionGroups(alertType.recoveryActionGroup); const { id, actionGroups, recoveryActionGroup } = alertType; - const activeActionGroups = new Set(actionGroups.map((item) => item.id)); - const intersectingReservedActionGroups = intersection( + const activeActionGroups = new Set(actionGroups.map((item) => item.id)); + const intersectingReservedActionGroups = intersection( [...activeActionGroups.values()], reservedActionGroups.map((item) => item.id) ); diff --git a/x-pack/plugins/alerts/server/index.ts b/x-pack/plugins/alerts/server/index.ts index 7bb54cd87bc33..da56da671f9b0 100644 --- a/x-pack/plugins/alerts/server/index.ts +++ b/x-pack/plugins/alerts/server/index.ts @@ -16,6 +16,7 @@ export { ActionVariable, AlertType, ActionGroup, + ActionGroupIdsOf, AlertingPlugin, AlertExecutorOptions, AlertActionParams, diff --git a/x-pack/plugins/alerts/server/lib/license_state.test.ts b/x-pack/plugins/alerts/server/lib/license_state.test.ts index 94db4c946ab00..2bba0a910b65e 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.test.ts @@ -56,7 +56,7 @@ describe('getLicenseCheckForAlertType', () => { let license: Subject; let licenseState: ILicenseState; const mockNotifyUsage = jest.fn(); - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -190,7 +190,7 @@ describe('ensureLicenseForAlertType()', () => { let license: Subject; let licenseState: ILicenseState; const mockNotifyUsage = jest.fn(); - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ diff --git a/x-pack/plugins/alerts/server/lib/license_state.ts b/x-pack/plugins/alerts/server/lib/license_state.ts index dea5b3338a5be..e20ccea7c834f 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.ts @@ -13,7 +13,13 @@ import { LicensingPluginStart } from '../../../licensing/server'; import { ILicense, LicenseType } from '../../../licensing/common/types'; import { PLUGIN } from '../constants/plugin'; import { getAlertTypeFeatureUsageName } from './get_alert_type_feature_usage_name'; -import { AlertType } from '../types'; +import { + AlertType, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../types'; import { AlertTypeDisabledError } from './errors/alert_type_disabled'; export type ILicenseState = PublicMethodsOf; @@ -130,7 +136,23 @@ export class LicenseState { } } - public ensureLicenseForAlertType(alertType: AlertType) { + public ensureLicenseForAlertType< + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string + >( + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + ) { this.notifyUsage(alertType.name, alertType.minimumLicenseRequired); const check = this.getLicenseCheckForAlertType( diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 6288d27c6ebe0..ece6fa2328d68 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -58,7 +58,7 @@ describe('Alerting Plugin', () => { describe('registerType()', () => { let setup: PluginSetupContract; - const sampleAlertType: AlertType = { + const sampleAlertType: AlertType = { id: 'test', name: 'test', minimumLicenseRequired: 'basic', diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 63861f5050f25..d15ae0ca55ef9 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -102,9 +102,18 @@ export interface PluginSetupContract { Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never, + RecoveryActionGroupId extends string = never >( - alertType: AlertType + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > ): void; } @@ -273,8 +282,19 @@ export class AlertingPlugin { Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext - >(alertType: AlertType) { + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never, + RecoveryActionGroupId extends string = never + >( + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + ) { if (!(alertType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${alertType.minimumLicenseRequired}" is not a valid license type`); } diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index 5603b13a3b1f5..5ab44a6ccdb51 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -15,7 +15,7 @@ import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { KibanaRequest } from 'kibana/server'; import { asSavedObjectExecutionSource } from '../../../actions/server'; import { InjectActionParamsOpts } from './inject_action_params'; -import { UntypedNormalizedAlertType } from '../alert_type_registry'; +import { NormalizedAlertType } from '../alert_type_registry'; import { AlertTypeParams, AlertTypeState, @@ -27,7 +27,14 @@ jest.mock('./inject_action_params', () => ({ injectActionParams: jest.fn(), })); -const alertType: UntypedNormalizedAlertType = { +const alertType: NormalizedAlertType< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + 'default' | 'other-group', + 'recovered' +> = { id: 'test', name: 'Test', actionGroups: [ @@ -53,7 +60,9 @@ const createExecutionHandlerParams: jest.Mocked< AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + 'default' | 'other-group', + 'recovered' > > = { actionsPlugin: mockActionsPlugin, @@ -348,7 +357,9 @@ test('state attribute gets parameterized', async () => { test(`logs an error when action group isn't part of actionGroups available for the alertType`, async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); const result = await executionHandler({ - actionGroup: 'invalid-group', + // we have to trick the compiler as this is an invalid type and this test checks whether we + // enforce this at runtime as well as compile time + actionGroup: 'invalid-group' as 'default' | 'other-group', context: {}, state: {}, alertInstanceId: '2', diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index 8b4412aeb23e5..c3d90c7bcf08b 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -27,7 +27,9 @@ export interface CreateExecutionHandlerOptions< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string > { alertId: string; alertName: string; @@ -36,26 +38,39 @@ export interface CreateExecutionHandlerOptions< actions: AlertAction[]; spaceId: string; apiKey: RawAlert['apiKey']; - alertType: NormalizedAlertType; + alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >; logger: Logger; eventLogger: IEventLogger; request: KibanaRequest; alertParams: AlertTypeParams; } -interface ExecutionHandlerOptions { - actionGroup: string; +interface ExecutionHandlerOptions { + actionGroup: ActionGroupIds; actionSubgroup?: string; alertInstanceId: string; context: AlertInstanceContext; state: AlertInstanceState; } +export type ExecutionHandler = ( + options: ExecutionHandlerOptions +) => Promise; + export function createExecutionHandler< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string >({ logger, alertId, @@ -69,7 +84,14 @@ export function createExecutionHandler< eventLogger, request, alertParams, -}: CreateExecutionHandlerOptions) { +}: CreateExecutionHandlerOptions< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId +>): ExecutionHandler { const alertTypeActionGroups = new Map( alertType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name]) ); @@ -79,7 +101,7 @@ export function createExecutionHandler< context, state, alertInstanceId, - }: ExecutionHandlerOptions) => { + }: ExecutionHandlerOptions) => { if (!alertTypeActionGroups.has(actionGroup)) { logger.error(`Invalid action group "${actionGroup}" for alert "${alertType.id}".`); return; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 967c5263b9730..75be9d371aee4 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -266,7 +266,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices .alertInstanceFactory('1') @@ -426,7 +427,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -542,7 +544,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); executorServices.alertInstanceFactory('2').scheduleActions('default'); @@ -595,7 +598,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -696,7 +700,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -743,7 +748,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices .alertInstanceFactory('1') @@ -798,7 +804,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -973,7 +980,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -1080,7 +1088,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -1178,7 +1187,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -1447,7 +1457,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { throw new Error('OMG'); } @@ -1822,7 +1833,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { throw new Error('OMG'); } diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index c4187145e5a16..12f7c33ae5052 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -10,7 +10,7 @@ import { addSpaceIdToPath } from '../../../spaces/server'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; -import { createExecutionHandler } from './create_execution_handler'; +import { createExecutionHandler, ExecutionHandler } from './create_execution_handler'; import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; import { validateAlertTypeParams, @@ -44,6 +44,7 @@ import { AlertTypeState, AlertInstanceState, AlertInstanceContext, + WithoutReservedActionGroups, } from '../../common'; import { NormalizedAlertType } from '../alert_type_registry'; @@ -64,16 +65,32 @@ export class TaskRunner< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string > { private context: TaskRunnerContext; private logger: Logger; private taskInstance: AlertTaskInstance; - private alertType: NormalizedAlertType; + private alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >; private readonly alertTypeRegistry: AlertTypeRegistry; constructor( - alertType: NormalizedAlertType, + alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >, taskInstance: ConcreteTaskInstance, context: TaskRunnerContext ) { @@ -144,7 +161,14 @@ export class TaskRunner< actions: Alert['actions'], alertParams: Params ) { - return createExecutionHandler({ + return createExecutionHandler< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >({ alertId, alertName, tags, @@ -163,7 +187,7 @@ export class TaskRunner< async executeAlertInstance( alertInstanceId: string, alertInstance: AlertInstance, - executionHandler: ReturnType + executionHandler: ExecutionHandler ) { const { actionGroup, @@ -180,7 +204,7 @@ export class TaskRunner< services: Services, alert: SanitizedAlert, params: Params, - executionHandler: ReturnType, + executionHandler: ExecutionHandler, spaceId: string, event: Event ): Promise { @@ -218,9 +242,11 @@ export class TaskRunner< alertId, services: { ...services, - alertInstanceFactory: createAlertInstanceFactory( - alertInstances - ), + alertInstanceFactory: createAlertInstanceFactory< + InstanceState, + InstanceContext, + WithoutReservedActionGroups + >(alertInstances), }, params, state: alertTypeState as State, @@ -278,7 +304,7 @@ export class TaskRunner< if (!muteAll) { const mutedInstanceIdsSet = new Set(mutedInstanceIds); - scheduleActionsForRecoveredInstances({ + scheduleActionsForRecoveredInstances({ recoveryActionGroup: this.alertType.recoveryActionGroup, recoveredAlertInstances, executionHandler, @@ -615,20 +641,30 @@ function generateNewAndRecoveredInstanceEvents< interface ScheduleActionsForRecoveredInstancesParams< InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + RecoveryActionGroupId extends string > { logger: Logger; - recoveryActionGroup: ActionGroup; - recoveredAlertInstances: Dictionary>; - executionHandler: ReturnType; + recoveryActionGroup: ActionGroup; + recoveredAlertInstances: Dictionary< + AlertInstance + >; + executionHandler: ExecutionHandler; mutedInstanceIdsSet: Set; alertLabel: string; } function scheduleActionsForRecoveredInstances< InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext ->(params: ScheduleActionsForRecoveredInstancesParams) { + InstanceContext extends AlertInstanceContext, + RecoveryActionGroupId extends string +>( + params: ScheduleActionsForRecoveredInstancesParams< + InstanceState, + InstanceContext, + RecoveryActionGroupId + > +) { const { logger, recoveryActionGroup, @@ -660,18 +696,31 @@ function scheduleActionsForRecoveredInstances< interface LogActiveAndRecoveredInstancesParams< InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string > { logger: Logger; - activeAlertInstances: Dictionary>; - recoveredAlertInstances: Dictionary>; + activeAlertInstances: Dictionary>; + recoveredAlertInstances: Dictionary< + AlertInstance + >; alertLabel: string; } function logActiveAndRecoveredInstances< InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext ->(params: LogActiveAndRecoveredInstancesParams) { + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +>( + params: LogActiveAndRecoveredInstancesParams< + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > +) { const { logger, activeAlertInstances, recoveredAlertInstances, alertLabel } = params; const activeInstanceIds = Object.keys(activeAlertInstances); const recoveredInstanceIds = Object.keys(recoveredAlertInstances); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index e266608d80880..2d57467075987 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -13,11 +13,19 @@ import { import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; -import { AlertTypeRegistry, GetServicesFunction, SpaceIdToNamespaceFunction } from '../types'; +import { + AlertTypeParams, + AlertTypeRegistry, + GetServicesFunction, + SpaceIdToNamespaceFunction, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../types'; import { TaskRunner } from './task_runner'; import { IEventLogger } from '../../../event_log/server'; import { AlertsClient } from '../alerts_client'; -import { UntypedNormalizedAlertType } from '../alert_type_registry'; +import { NormalizedAlertType } from '../alert_type_registry'; export interface TaskRunnerContext { logger: Logger; @@ -44,11 +52,35 @@ export class TaskRunnerFactory { this.taskRunnerContext = taskRunnerContext; } - public create(alertType: UntypedNormalizedAlertType, { taskInstance }: RunContext) { + public create< + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string + >( + alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >, + { taskInstance }: RunContext + ) { if (!this.isInitialized) { throw new Error('TaskRunnerFactory not initialized'); } - return new TaskRunner(alertType, taskInstance, this.taskRunnerContext!); + return new TaskRunner< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >(alertType, taskInstance, this.taskRunnerContext!); } } diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index bb2d429a7c8b5..39c52d9653aaa 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -29,6 +29,7 @@ import { AlertExecutionStatusErrorReasons, AlertsHealth, AlertNotifyWhenType, + WithoutReservedActionGroups, } from '../common'; import { LicenseType } from '../../licensing/server'; @@ -58,21 +59,25 @@ export interface Services { export interface AlertServices< InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never > extends Services { - alertInstanceFactory: (id: string) => PublicAlertInstance; + alertInstanceFactory: ( + id: string + ) => PublicAlertInstance; } export interface AlertExecutorOptions< Params extends AlertTypeParams = never, State extends AlertTypeState = never, InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never > { alertId: string; startedAt: Date; previousStartedAt: Date | null; - services: AlertServices; + services: AlertServices; params: Params; state: State; spaceId: string; @@ -92,9 +97,10 @@ export type ExecutorType< Params extends AlertTypeParams = never, State extends AlertTypeState = never, InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never > = ( - options: AlertExecutorOptions + options: AlertExecutorOptions ) => Promise; export interface AlertTypeParamsValidator { @@ -104,17 +110,29 @@ export interface AlertType< Params extends AlertTypeParams = never, State extends AlertTypeState = never, InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never, + RecoveryActionGroupId extends string = never > { id: string; name: string; validate?: { params?: AlertTypeParamsValidator; }; - actionGroups: ActionGroup[]; - defaultActionGroupId: ActionGroup['id']; - recoveryActionGroup?: ActionGroup; - executor: ExecutorType; + actionGroups: Array>; + defaultActionGroupId: ActionGroup['id']; + recoveryActionGroup?: ActionGroup; + executor: ExecutorType< + Params, + State, + InstanceState, + InstanceContext, + /** + * Ensure that the reserved ActionGroups (such as `Recovered`) are not + * available for scheduling in the Executor + */ + WithoutReservedActionGroups + >; producer: string; actionVariables?: { context?: ActionVariable[]; diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index 7cc36253ef581..bb42c8acd167a 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { ValuesType } from 'utility-types'; +import { ActionGroup } from '../../alerts/common'; import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../../ml/common'; export enum AlertType { @@ -15,20 +16,31 @@ export enum AlertType { TransactionDurationAnomaly = 'apm.transaction_duration_anomaly', } -const THRESHOLD_MET_GROUP = { - id: 'threshold_met', +export const THRESHOLD_MET_GROUP_ID = 'threshold_met'; +export type ThresholdMetActionGroupId = typeof THRESHOLD_MET_GROUP_ID; +const THRESHOLD_MET_GROUP: ActionGroup = { + id: THRESHOLD_MET_GROUP_ID, name: i18n.translate('xpack.apm.a.thresholdMet', { defaultMessage: 'Threshold met', }), }; -export const ALERT_TYPES_CONFIG = { +export const ALERT_TYPES_CONFIG: Record< + AlertType, + { + name: string; + actionGroups: Array>; + defaultActionGroupId: ThresholdMetActionGroupId; + minimumLicenseRequired: string; + producer: string; + } +> = { [AlertType.ErrorCount]: { name: i18n.translate('xpack.apm.errorCountAlert.name', { defaultMessage: 'Error count threshold', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, @@ -37,7 +49,7 @@ export const ALERT_TYPES_CONFIG = { defaultMessage: 'Transaction duration threshold', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, @@ -46,7 +58,7 @@ export const ALERT_TYPES_CONFIG = { defaultMessage: 'Transaction duration anomaly', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, @@ -55,7 +67,7 @@ export const ALERT_TYPES_CONFIG = { defaultMessage: 'Transaction error rate threshold', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index 2f05842b6bdec..e7ce4bb24b38f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -5,6 +5,7 @@ */ import { render } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; import { CoreStart } from 'kibana/public'; import React, { ReactNode } from 'react'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; @@ -15,6 +16,10 @@ import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_ap import { LicenseContext } from '../../../context/license/license_context'; import * as useFetcherModule from '../../../hooks/use_fetcher'; import { ServiceMap } from './'; +import { UrlParamsProvider } from '../../../context/url_params_context/url_params_context'; +import { Router } from 'react-router-dom'; + +const history = createMemoryHistory(); const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -49,7 +54,9 @@ function createWrapper(license: License | null) { - {children} + + {children} + diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 48a7f8f77ab84..da4a8596970ec 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -39,21 +39,34 @@ const ServiceMapDatePickerFlexGroup = styled(EuiFlexGroup)` margin: 0; `; +function DatePickerSection() { + return ( + + + + + + ); +} + function PromptContainer({ children }: { children: ReactNode }) { return ( - - + + - {children} - - + + {children} + + + ); } @@ -137,11 +150,7 @@ export function ServiceMap({ return ( <> - - - - - +
( - services: AlertServices, + services: AlertServices< + AlertInstanceState, + AlertInstanceContext, + ThresholdMetActionGroupId + >, params: TParams ): Promise> { return services.callCluster('search', { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 36fdf45d805f1..764e706834a70 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -4,13 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { isEmpty } from 'lodash'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { APMConfig } from '../..'; -import { AlertingPlugin } from '../../../../alerts/server'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertingPlugin, + AlertInstanceContext, + AlertInstanceState, + AlertTypeState, +} from '../../../../alerts/server'; +import { + AlertType, + ALERT_TYPES_CONFIG, + ThresholdMetActionGroupId, +} from '../../../common/alert_types'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, @@ -41,7 +50,13 @@ export function registerErrorCountAlertType({ alerts, config$, }: RegisterAlertParams) { - alerts.registerType({ + alerts.registerType< + TypeOf, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + ThresholdMetActionGroupId + >({ id: AlertType.ErrorCount, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index 5eeb336ad1108..4e1ec76c52a77 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -178,7 +178,7 @@ export const ColdPhase: FunctionComponent = () => { id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText" defaultMessage="Make the index read-only and minimize its memory footprint." />{' '} - + } fullWidth diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index 224194744de51..a777f30fd2e42 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -114,7 +114,7 @@ export const HotPhase: FunctionComponent = () => { defaultMessage="Learn more" /> } - docPath="indices-rollover-index.html" + docPath="ilm-rollover.html" />

diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx index 69121cc2d1252..8776dbbbc7553 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx @@ -43,7 +43,7 @@ export const ForcemergeField: React.FunctionComponent = ({ phase }) => { id="xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableExplanationText" defaultMessage="Reduce the number of segments in your shard by merging smaller files and clearing deleted ones." />{' '} - + } titleSize="xs" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx index e5ec1d116ec6f..328587a379b76 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx @@ -36,7 +36,7 @@ export const SetPriorityInputField: FunctionComponent = ({ phase }) => { defaultMessage="Set the priority for recovering your indices after a node restart. Indices with higher priorities are recovered before indices with lower priorities." />{' '} - + } titleSize="xs" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx index f1cfbeb3692f7..c5fc31d9839bd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx @@ -37,7 +37,7 @@ export const ShrinkField: FunctionComponent = ({ phase }) => { id="xpack.indexLifecycleMgmt.editPolicy.shrinkIndexExplanationText" defaultMessage="Shrink the index into a new index with fewer primary shards." />{' '} - + } titleSize="xs" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts index 7f148b9735a04..1aa372ff4043c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -172,6 +172,20 @@ describe('deserializer and serializer', () => { expect(result.phases.warm!.actions.forcemerge).toBeUndefined(); }); + it('removes the index_codec option in the forcemerge action if it is disabled in the form', () => { + formInternal.phases.warm!.actions.forcemerge = { + max_num_segments: 22, + index_codec: 'best_compression', + }; + formInternal._meta.hot.bestCompression = false; + formInternal._meta.warm.bestCompression = false; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.forcemerge!.index_codec).toBeUndefined(); + expect(result.phases.warm!.actions.forcemerge!.index_codec).toBeUndefined(); + }); + it('removes the readonly action if it is disabled in hot', () => { formInternal._meta.hot.readonlyEnabled = false; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 362703672a89f..599bbfb81d1d8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -81,6 +81,8 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete hotPhaseActions.forcemerge; } else if (_meta.hot.bestCompression) { hotPhaseActions.forcemerge!.index_codec = 'best_compression'; + } else { + delete hotPhaseActions.forcemerge!.index_codec; } if (_meta.hot.bestCompression && hotPhaseActions.forcemerge) { @@ -163,6 +165,8 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete warmPhase.actions.forcemerge; } else if (_meta.warm.bestCompression) { warmPhase.actions.forcemerge!.index_codec = 'best_compression'; + } else { + delete warmPhase.actions.forcemerge!.index_codec; } /** diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx index d66fd44feba56..f1e796ef8ba18 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -222,9 +222,9 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible - + - + @@ -240,7 +240,7 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible - + props.theme.eui.paddingSizes.s} ${(props) => props.theme.eui.paddingSizes.m}; + @media only screen and (max-width: 767px) { + margin-top: 30px; + } `; const TimelineChartContainer = euiStyled.div` diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 1c1baad30f473..f4da68d9dead7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -186,9 +186,8 @@ export const LegendControls = ({ button={buttonComponent} > Legend Options - + @@ -243,6 +240,10 @@ export const LegendControls = ({ checked={draftLegend.reverseColors} onChange={handleReverseColors} compressed + style={{ + position: 'relative', + top: '8px', + }} /> - + { }; const Swatch = euiStyled.div` - width: 16px; + width: 15px; height: 12px; flex: 0 0 auto; &:first-child { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx index ae64188f8a469..f4cec07b53b3b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx @@ -14,7 +14,7 @@ export interface Props { export const SwatchLabel = ({ label, color }: Props) => { return ( - + diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 54cf8658a3f0d..6d851eeaab542 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -10,6 +10,7 @@ import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_m import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertStates, InventoryMetricConditions } from './types'; import { + ActionGroup, AlertInstanceContext, AlertInstanceState, RecoveredActionGroup, @@ -27,6 +28,7 @@ import { stateToAlertMessage, } from '../common/messages'; import { evaluateCondition } from './evaluate_condition'; +import { InventoryMetricThresholdAllowedActionGroups } from './register_inventory_metric_threshold_alert_type'; interface InventoryMetricThresholdParams { criteria: InventoryMetricConditions[]; @@ -46,7 +48,8 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = Record, Record, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + InventoryMetricThresholdAllowedActionGroups >) => { const { criteria, @@ -115,18 +118,25 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } if (reason) { const actionGroupId = - nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; - alertInstance.scheduleActions(actionGroupId, { - group: item, - alertState: stateToAlertMessage[nextState], - reason, - timestamp: moment().toISOString(), - value: mapToConditionsLookup(results, (result) => - formatMetric(result[item].metric, result[item].currentValue) - ), - threshold: mapToConditionsLookup(criteria, (c) => c.threshold), - metric: mapToConditionsLookup(criteria, (c) => c.metric), - }); + nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS_ID; + alertInstance.scheduleActions( + /** + * TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on + * the RecoveredActionGroup isn't allowed + */ + (actionGroupId as unknown) as InventoryMetricThresholdAllowedActionGroups, + { + group: item, + alertState: stateToAlertMessage[nextState], + reason, + timestamp: moment().toISOString(), + value: mapToConditionsLookup(results, (result) => + formatMetric(result[item].metric, result[item].currentValue) + ), + threshold: mapToConditionsLookup(criteria, (c) => c.threshold), + metric: mapToConditionsLookup(criteria, (c) => c.metric), + } + ); } alertInstance.replaceState({ @@ -160,8 +170,9 @@ const mapToConditionsLookup = ( {} ); -export const FIRED_ACTIONS = { - id: 'metrics.invenotry_threshold.fired', +export const FIRED_ACTIONS_ID = 'metrics.invenotry_threshold.fired'; +export const FIRED_ACTIONS: ActionGroup = { + id: FIRED_ACTIONS_ID, name: i18n.translate('xpack.infra.metrics.alerting.inventory.threshold.fired', { defaultMessage: 'Fired', }), diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index a2e8eff34ef98..48efe8fd45a3c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -9,6 +9,7 @@ import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../.. import { createInventoryMetricThresholdExecutor, FIRED_ACTIONS, + FIRED_ACTIONS_ID, } from './inventory_metric_threshold_executor'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; import { InfraBackendLibs } from '../../infra_types'; @@ -22,6 +23,7 @@ import { metricActionVariableDescription, thresholdActionVariableDescription, } from '../common/messages'; +import { RecoveredActionGroupId } from '../../../../../alerts/common'; const condition = schema.object({ threshold: schema.arrayOf(schema.number()), @@ -40,6 +42,8 @@ const condition = schema.object({ ), }); +export type InventoryMetricThresholdAllowedActionGroups = typeof FIRED_ACTIONS_ID; + export const registerMetricInventoryThresholdAlertType = ( libs: InfraBackendLibs ): AlertType< @@ -49,7 +53,9 @@ export const registerMetricInventoryThresholdAlertType = ( Record, Record, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + InventoryMetricThresholdAllowedActionGroups, + RecoveredActionGroupId > => ({ id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.metrics.inventory.alertName', { @@ -69,7 +75,7 @@ export const registerMetricInventoryThresholdAlertType = ( { unknowns: 'allow' } ), }, - defaultActionGroupId: FIRED_ACTIONS.id, + defaultActionGroupId: FIRED_ACTIONS_ID, actionGroups: [FIRED_ACTIONS], producer: 'infrastructure', minimumLicenseRequired: 'basic', diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 09d7e482772c2..f4a9e8fdef3ff 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -13,6 +13,8 @@ import { AlertTypeState, AlertInstanceContext, AlertInstanceState, + ActionGroup, + ActionGroupIdsOf, } from '../../../../../alerts/server'; import { AlertStates, @@ -37,12 +39,18 @@ import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; -type LogThresholdAlertServices = AlertServices; +type LogThresholdActionGroups = ActionGroupIdsOf; +type LogThresholdAlertServices = AlertServices< + AlertInstanceState, + AlertInstanceContext, + LogThresholdActionGroups +>; type LogThresholdAlertExecutorOptions = AlertExecutorOptions< AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + LogThresholdActionGroups >; const COMPOSITE_GROUP_SIZE = 40; @@ -344,9 +352,9 @@ export const processGroupByRatioResults = ( }; type AlertInstanceUpdater = ( - alertInstance: AlertInstance, + alertInstance: AlertInstance, state: AlertStates, - actions?: Array<{ actionGroup: string; context: AlertInstanceContext }> + actions?: Array<{ actionGroup: LogThresholdActionGroups; context: AlertInstanceContext }> ) => void; export const updateAlertInstance: AlertInstanceUpdater = (alertInstance, state, actions) => { @@ -653,8 +661,9 @@ const createConditionsMessageForCriteria = (criteria: CountCriteria) => { // When the Alerting plugin implements support for multiple action groups, add additional // action groups here to send different messages, e.g. a recovery notification -export const FIRED_ACTIONS = { - id: 'logs.threshold.fired', +export const LogsThresholdFiredActionGroupId = 'logs.threshold.fired'; +export const FIRED_ACTIONS: ActionGroup<'logs.threshold.fired'> = { + id: LogsThresholdFiredActionGroupId, name: i18n.translate('xpack.infra.logs.alerting.threshold.fired', { defaultMessage: 'Fired', }), diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index e248d3b3ddcfa..236ab9b97fdc3 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { PluginSetupContract } from '../../../../../alerts/server'; +import { + PluginSetupContract, + AlertTypeParams, + AlertTypeState, + AlertInstanceContext, + AlertInstanceState, + ActionGroupIdsOf, +} from '../../../../../alerts/server'; import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, @@ -79,7 +86,13 @@ export async function registerLogThresholdAlertType( ); } - alertingPlugin.registerType({ + alertingPlugin.registerType< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + ActionGroupIdsOf + >({ id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.logs.alertName', { defaultMessage: 'Log threshold', diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 000c89f5899ef..77126e7d9454c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -10,6 +10,7 @@ import { AlertInstanceState, AlertInstanceContext, AlertExecutorOptions, + ActionGroupIdsOf, } from '../../../../../alerts/server'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; @@ -33,7 +34,8 @@ export type MetricThresholdAlertType = AlertType< Record, Record, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + ActionGroupIdsOf >; export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions< /** @@ -42,7 +44,8 @@ export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions< Record, Record, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + ActionGroupIdsOf >; export function registerMetricThresholdAlertType(libs: InfraBackendLibs): MetricThresholdAlertType { diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index eea201dcc8baa..5c9b9a323fbff 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -39,10 +39,15 @@ type ESGeoLineSourceSyncMeta = { sortField: string; }; +type ESTermSourceSyncMeta = { + size: number; +}; + export type VectorSourceSyncMeta = | ESSearchSourceSyncMeta | ESGeoGridSourceSyncMeta | ESGeoLineSourceSyncMeta + | ESTermSourceSyncMeta | null; export type VectorSourceRequestMeta = MapFilters & { @@ -54,10 +59,9 @@ export type VectorSourceRequestMeta = MapFilters & { sourceMeta: VectorSourceSyncMeta; }; -export type VectorJoinSourceRequestMeta = Omit< - VectorSourceRequestMeta, - 'geogridPrecision' | 'sourceMeta' -> & { sourceQuery?: Query }; +export type VectorJoinSourceRequestMeta = Omit & { + sourceQuery?: Query; +}; export type VectorStyleRequestMeta = MapFilters & { dynamicStyleFields: string[]; diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts index 46b799835b9e5..603e1d767e1c6 100644 --- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts @@ -104,6 +104,7 @@ export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & { indexPatternTitle?: string; term: string; // term field name whereQuery?: Query; + size?: number; }; export type KibanaRegionmapSourceDescriptor = AbstractSourceDescriptor & { diff --git a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts index 17dc84ead299b..e21f5ac5547bc 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts @@ -8,6 +8,7 @@ import uuid from 'uuid/v4'; import { AggDescriptor, ColorDynamicOptions, + ESTermSourceDescriptor, LayerDescriptor, } from '../../../common/descriptor_types'; import { @@ -48,6 +49,7 @@ export function createRegionMapLayerDescriptor({ emsLayerId, leftFieldName, termsFieldName, + termsSize, colorSchema, indexPatternId, indexPatternTitle, @@ -58,6 +60,7 @@ export function createRegionMapLayerDescriptor({ emsLayerId?: string; leftFieldName?: string; termsFieldName?: string; + termsSize?: number; colorSchema: string; indexPatternId?: string; indexPatternTitle?: string; @@ -78,21 +81,25 @@ export function createRegionMapLayerDescriptor({ const colorPallette = NUMERICAL_COLOR_PALETTES.find((pallette) => { return pallette.value.toLowerCase() === colorSchema.toLowerCase(); }); + const termSourceDescriptor: ESTermSourceDescriptor = { + type: SOURCE_TYPES.ES_TERM_SOURCE, + id: joinId, + indexPatternId, + indexPatternTitle: indexPatternTitle ? indexPatternTitle : indexPatternId, + term: termsFieldName, + metrics: [metricsDescriptor], + applyGlobalQuery: true, + applyGlobalTime: true, + }; + if (termsSize !== undefined) { + termSourceDescriptor.size = termsSize; + } return VectorLayer.createDescriptor({ label, joins: [ { leftField: leftFieldName, - right: { - type: SOURCE_TYPES.ES_TERM_SOURCE, - id: joinId, - indexPatternId, - indexPatternTitle: indexPatternTitle ? indexPatternTitle : indexPatternId, - term: termsFieldName, - metrics: [metricsDescriptor], - applyGlobalQuery: true, - applyGlobalTime: true, - }, + right: termSourceDescriptor, }, ], sourceDescriptor: EMSFileSource.createDescriptor({ diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 7ea5ad25a99b4..add5a980258f3 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -349,6 +349,7 @@ export class VectorLayer extends AbstractLayer { sourceQuery: joinSource.getWhereQuery(), applyGlobalQuery: joinSource.getApplyGlobalQuery(), applyGlobalTime: joinSource.getApplyGlobalTime(), + sourceMeta: joinSource.getSyncMeta(), }; const prevDataRequest = this.getDataRequest(sourceDataId); diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts index 9387db2e151d9..12f1ef4829a4a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts @@ -25,6 +25,7 @@ import { import { ESTermSourceDescriptor, VectorJoinSourceRequestMeta, + VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; import { PropertiesMap } from '../../../../common/elasticsearch_util'; @@ -124,7 +125,9 @@ export class ESTermSource extends AbstractESAggSource { const indexPattern = await this.getIndexPattern(); const searchSource: ISearchSource = await this.makeSearchSource(searchFilters, 0); const termsField = getField(indexPattern, this._termField.getName()); - const termsAgg = { size: DEFAULT_MAX_BUCKETS_LIMIT }; + const termsAgg = { + size: this._descriptor.size !== undefined ? this._descriptor.size : DEFAULT_MAX_BUCKETS_LIMIT, + }; searchSource.setField('aggs', { [TERMS_AGG_NAME]: { terms: addFieldToDSL(termsAgg, termsField), @@ -162,4 +165,12 @@ export class ESTermSource extends AbstractESAggSource { getFieldNames(): string[] { return this.getMetricFields().map((esAggMetricField) => esAggMetricField.getName()); } + + getSyncMeta(): VectorSourceSyncMeta | null { + return this._descriptor.size !== undefined + ? { + size: this._descriptor.size, + } + : null; + } } diff --git a/x-pack/plugins/maps/public/components/__snapshots__/validated_number_input.test.tsx.snap b/x-pack/plugins/maps/public/components/__snapshots__/validated_number_input.test.tsx.snap index 0ae98e2fd4508..75945c82a0118 100644 --- a/x-pack/plugins/maps/public/components/__snapshots__/validated_number_input.test.tsx.snap +++ b/x-pack/plugins/maps/public/components/__snapshots__/validated_number_input.test.tsx.snap @@ -3,7 +3,7 @@ exports[`should render with error 1`] = ` ); } diff --git a/x-pack/plugins/maps/public/components/validated_number_input.tsx b/x-pack/plugins/maps/public/components/validated_number_input.tsx index a79faa73ced04..5d872e96b4e6b 100644 --- a/x-pack/plugins/maps/public/components/validated_number_input.tsx +++ b/x-pack/plugins/maps/public/components/validated_number_input.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, ChangeEvent } from 'react'; -import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import React, { Component, ChangeEvent, ReactNode } from 'react'; +// @ts-expect-error +import { EuiFieldNumber, EuiFormRow, EuiFormRowDisplayKeys } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import _ from 'lodash'; @@ -21,6 +22,8 @@ interface Props { max: number; onChange: (value: number) => void; label: string; + display?: EuiFormRowDisplayKeys; + helpText?: ReactNode; } function getErrorMessage(min: number, max: number): string { @@ -97,7 +100,8 @@ export class ValidatedNumberInput extends Component { label={this.props.label} isInvalid={!this.state.isValid} error={this.state.errorMessage ? [this.state.errorMessage] : []} - display="columnCompressed" + display={this.props.display} + helpText={this.props.helpText} >
{ + this.props.onChange({ + leftField: this.props.join.leftField, + right: { + ...this.props.join.right, + size, + }, + }); + }; + _onMetricsChange = (metrics) => { this.props.onChange({ leftField: this.props.join.leftField, @@ -161,7 +173,7 @@ export class Join extends Component { ); globalFilterCheckbox = ( diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js index c6c784481436c..fa01115573fde 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js @@ -17,7 +17,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { DEFAULT_MAX_BUCKETS_LIMIT } from '../../../../../common/constants'; import { SingleFieldSelect } from '../../../../components/single_field_select'; +import { ValidatedNumberInput } from '../../../../components/validated_number_input'; import { FormattedMessage } from '@kbn/i18n/react'; import { getTermsFields } from '../../../../index_pattern_util'; @@ -155,10 +157,49 @@ export class JoinExpression extends Component { ); } + _renderRightFieldSizeInput() { + if (!this.props.rightValue || !this.props.leftValue) { + return null; + } + + return ( + + ); + } + _getExpressionValue() { - const { leftSourceName, leftValue, rightSourceName, rightValue } = this.props; + const { leftSourceName, leftValue, rightSourceName, rightValue, rightSize } = this.props; if (leftSourceName && leftValue && rightSourceName && rightValue) { - return `${leftSourceName}:${leftValue} with ${rightSourceName}:${rightValue}`; + return i18n.translate('xpack.maps.layerPanel.joinExpression.value', { + defaultMessage: + '{leftSourceName}:{leftValue} with {sizeFragment} {rightSourceName}:{rightValue}', + values: { + leftSourceName, + leftValue, + sizeFragment: + rightSize !== undefined + ? i18n.translate('xpack.maps.layerPanel.joinExpression.sizeFragment', { + defaultMessage: 'top {rightSize} terms from', + values: { rightSize }, + }) + : '', + rightSourceName, + rightValue, + }, + }); } return i18n.translate('xpack.maps.layerPanel.joinExpression.selectPlaceholder', { @@ -213,6 +254,8 @@ export class JoinExpression extends Component { {this._renderRightSourceSelect()} {this._renderRightFieldSelect()} + + {this._renderRightFieldSizeInput()}
); @@ -240,8 +283,10 @@ JoinExpression.propTypes = { // Right field props rightValue: PropTypes.string, + rightSize: PropTypes.number, rightFields: PropTypes.array, onRightFieldChange: PropTypes.func.isRequired, + onRightSizeChange: PropTypes.func.isRequired, }; function getSelectFieldPlaceholder() { diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js index 6b119ba6d850d..a66236dc9dbf2 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js @@ -93,7 +93,6 @@ export class MetricsExpression extends Component { closePopover={this._closePopover} ownFocus initialFocus="body" /* avoid initialFocus on Combobox */ - withTitle anchorPosition="leftCenter" button={ { const { id, name, actionVariables } = this.alertOptions; return { id, @@ -108,8 +108,11 @@ export class BaseAlert { ], defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', - executor: (options: AlertExecutorOptions & { state: ExecutedState }): Promise => - this.execute(options), + executor: ( + options: AlertExecutorOptions & { + state: ExecutedState; + } + ): Promise => this.execute(options), producer: 'monitoring', actionVariables: { context: actionVariables, @@ -238,7 +241,9 @@ export class BaseAlert { services, params, state, - }: AlertExecutorOptions & { state: ExecutedState }): Promise { + }: AlertExecutorOptions & { + state: ExecutedState; + }): Promise { this.scopedLogger.debug( `Executing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` ); @@ -333,7 +338,7 @@ export class BaseAlert { protected async processData( data: AlertData[], clusters: AlertCluster[], - services: AlertServices, + services: AlertServices, state: ExecutedState ) { const currentUTC = +new Date(); @@ -387,7 +392,7 @@ export class BaseAlert { protected async processLegacyData( data: AlertData[], clusters: AlertCluster[], - services: AlertServices, + services: AlertServices, state: ExecutedState ) { const currentUTC = +new Date(); diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index 154a05742d747..9e533d9758521 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -117,28 +117,6 @@ describe('Reporting server createConfig$', () => { expect((mockLogger.warn as any).mock.calls.length).toBe(0); }); - it('show warning when kibanaServer.hostName === "0"', async () => { - mockInitContext = makeMockInitContext({ - encryptionKey: 'aaaaaaaaaaaaabbbbbbbbbbbbaaaaaaaaa', - kibanaServer: { hostname: '0' }, - }); - const mockConfig$: any = mockInitContext.config.create(); - const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); - - expect(result.kibanaServer).toMatchInlineSnapshot(` - Object { - "hostname": "0.0.0.0", - "port": 5601, - "protocol": "http", - } - `); - expect((mockLogger.warn as any).mock.calls.length).toBe(1); - expect((mockLogger.warn as any).mock.calls[0]).toMatchObject([ - `Found 'server.host: \"0\"' in Kibana configuration. This is incompatible with Reporting. To enable Reporting to work, 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' is being automatically ` + - `to the configuration. You can change the setting to 'server.host: 0.0.0.0' or add 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' in kibana.yml to prevent this message.`, - ]); - }); - it('uses user-provided disableSandbox: false', async () => { mockInitContext = makeMockInitContext({ encryptionKey: '888888888888888888888888888888888', diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts index 2e07478c1663c..1f3d00540e81c 100644 --- a/x-pack/plugins/reporting/server/config/create_config.ts +++ b/x-pack/plugins/reporting/server/config/create_config.ts @@ -42,26 +42,12 @@ export function createConfig$( } const { kibanaServer: reportingServer } = config; const serverInfo = core.http.getServerInfo(); - // kibanaServer.hostname, default to server.host, don't allow "0" - let kibanaServerHostname = reportingServer.hostname + // kibanaServer.hostname, default to server.host + const kibanaServerHostname = reportingServer.hostname ? reportingServer.hostname : serverInfo.hostname; - if (kibanaServerHostname === '0') { - logger.warn( - i18n.translate('xpack.reporting.serverConfig.invalidServerHostname', { - defaultMessage: - `Found 'server.host: "0"' in Kibana configuration. This is incompatible with Reporting. ` + - `To enable Reporting to work, '{configKey}: 0.0.0.0' is being automatically to the configuration. ` + - `You can change the setting to 'server.host: 0.0.0.0' or add '{configKey}: 0.0.0.0' in kibana.yml to prevent this message.`, - values: { configKey: 'xpack.reporting.kibanaServer.hostname' }, - }) - ); - kibanaServerHostname = '0.0.0.0'; - } // kibanaServer.port, default to server.port - const kibanaServerPort = reportingServer.port - ? reportingServer.port - : serverInfo.port; // prettier-ignore + const kibanaServerPort = reportingServer.port ? reportingServer.port : serverInfo.port; // kibanaServer.protocol, default to server.protocol const kibanaServerProtocol = reportingServer.protocol ? reportingServer.protocol diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index 763231f7fd0df..5ccf2ead0a8c8 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -42,6 +42,13 @@ function getMockPeerCertificate(chain: string[] | string) { // Imitate self-signed certificate that is issuer for itself. certificate.issuerCertificate = index === fingerprintChain.length - 1 ? certificate : {}; + // Imitate other fields for logging assertions + certificate.subject = 'mock subject'; + certificate.issuer = 'mock issuer'; + certificate.subjectaltname = 'mock subjectaltname'; + certificate.valid_from = 'mock valid_from'; + certificate.valid_to = 'mock valid_to'; + return certificate.issuerCertificate; }, mockPeerCertificate as Record @@ -59,6 +66,9 @@ function getMockSocket({ } = {}) { const socket = new TLSSocket(new Socket()); socket.authorized = authorized; + if (!authorized) { + socket.authorizationError = new Error('mock authorization error'); + } socket.getPeerCertificate = jest.fn().mockReturnValue(peerCertificate); return socket; } @@ -88,26 +98,58 @@ describe('PKIAuthenticationProvider', () => { function defineCommonLoginAndAuthenticateTests( operation: (request: KibanaRequest) => Promise ) { - it('does not handle requests without certificate.', async () => { + it('does not handle unauthorized requests.', async () => { const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ authorized: true }), + socket: getMockSocket({ + authorized: false, + peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'), + }), }); await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.logger.debug).toHaveBeenCalledWith( + 'Peer certificate chain: [{"subject":"mock subject","issuer":"mock issuer","issuerCertType":"object","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]' + ); + expect(mockOptions.logger.debug).toHaveBeenCalledWith( + 'Authentication is not possible since peer certificate was not authorized: Error: mock authorization error.' + ); }); - it('does not handle unauthorized requests.', async () => { + it('does not handle requests with a missing certificate chain.', async () => { const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ peerCertificate: getMockPeerCertificate('2A:7A:C2:DD') }), + socket: getMockSocket({ authorized: true, peerCertificate: null }), }); await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.logger.debug).toHaveBeenCalledWith('Peer certificate chain: []'); + expect(mockOptions.logger.debug).toHaveBeenCalledWith( + 'Authentication is not possible due to missing peer certificate chain.' + ); + }); + + it('does not handle requests with an incomplete certificate chain.', async () => { + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD'); + (peerCertificate as any).issuerCertificate = undefined; // This behavior has been observed, even though it's not valid according to the type definition + const request = httpServerMock.createKibanaRequest({ + socket: getMockSocket({ authorized: true, peerCertificate }), + }); + + await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.logger.debug).toHaveBeenCalledWith( + 'Peer certificate chain: [{"subject":"mock subject","issuer":"mock issuer","issuerCertType":"undefined","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]' + ); + expect(mockOptions.logger.debug).toHaveBeenCalledWith( + 'Authentication is not possible due to incomplete peer certificate chain.' + ); }); it('gets an access token in exchange to peer certificate chain and stores it in the state.', async () => { diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index 4bb0ddaa4ee65..5642a6feac2b5 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -40,6 +40,39 @@ function canStartNewSession(request: KibanaRequest) { return canRedirectRequest(request) && request.route.options.authRequired === true; } +/** + * Returns a stringified version of a certificate, including metadata + * @param peerCertificate DetailedPeerCertificate instance. + */ +function stringifyCertificate(peerCertificate: DetailedPeerCertificate) { + const { + subject, + issuer, + issuerCertificate, + subjectaltname, + valid_from: validFrom, + valid_to: validTo, + } = peerCertificate; + + // The issuerCertificate field can be three different values: + // * Object: In this case, the issuer certificate is an object + // * null: In this case, the issuer certificate is a null value; this should not happen according to the type definition but historically there was code in place to account for this + // * undefined: The issuer certificate chain is broken; this should not happen according to the type definition but we have observed this edge case behavior with certain client/server configurations + // This distinction can be useful for troubleshooting mutual TLS connection problems, so we include it in the stringified certificate that is printed to the debug logs. + // There are situations where a partial client certificate chain is accepted by Node, but we cannot verify the chain in Kibana because an intermediate issuerCertificate is undefined. + // If this happens, Kibana will reject the authentication attempt, and the client and/or server need to ensure that the entire CA chain is installed. + let issuerCertType: string; + if (issuerCertificate === undefined) { + issuerCertType = 'undefined'; + } else if (issuerCertificate === null) { + issuerCertType = 'null'; + } else { + issuerCertType = 'object'; + } + + return JSON.stringify({ subject, issuer, issuerCertType, subjectaltname, validFrom, validTo }); +} + /** * Provider that supports PKI request authentication. */ @@ -204,6 +237,10 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { private async authenticateViaPeerCertificate(request: KibanaRequest) { this.logger.debug('Trying to authenticate request via peer certificate chain.'); + // We should collect entire certificate chain as an ordered array of certificates encoded as base64 strings. + const peerCertificate = request.socket.getPeerCertificate(true); + const { certificateChain, isChainIncomplete } = this.getCertificateChain(peerCertificate); + if (!request.socket.authorized) { this.logger.debug( `Authentication is not possible since peer certificate was not authorized: ${request.socket.authorizationError}.` @@ -211,14 +248,16 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { return AuthenticationResult.notHandled(); } - const peerCertificate = request.socket.getPeerCertificate(true); if (peerCertificate === null) { this.logger.debug('Authentication is not possible due to missing peer certificate chain.'); return AuthenticationResult.notHandled(); } - // We should collect entire certificate chain as an ordered array of certificates encoded as base64 strings. - const certificateChain = this.getCertificateChain(peerCertificate); + if (isChainIncomplete) { + this.logger.debug('Authentication is not possible due to incomplete peer certificate chain.'); + return AuthenticationResult.notHandled(); + } + let result: { access_token: string; authentication: AuthenticationInfo }; try { result = await this.options.client.callAsInternalUser('shield.delegatePKI', { @@ -255,23 +294,31 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { */ private getCertificateChain(peerCertificate: DetailedPeerCertificate | null) { const certificateChain = []; + const certificateStrings = []; + let isChainIncomplete = false; let certificate: DetailedPeerCertificate | null = peerCertificate; - while (certificate !== null && Object.keys(certificate).length > 0) { + + while (certificate && Object.keys(certificate).length > 0) { certificateChain.push(certificate.raw.toString('base64')); + certificateStrings.push(stringifyCertificate(certificate)); // For self-signed certificates, `issuerCertificate` may be a circular reference. if (certificate === certificate.issuerCertificate) { this.logger.debug('Self-signed certificate is detected in certificate chain'); - certificate = null; + break; + } else if (certificate.issuerCertificate === undefined) { + // The chain is only considered to be incomplete if one or more issuerCertificate values is undefined; + // this is not an expected return value from Node, but it can happen in some edge cases + isChainIncomplete = true; + break; } else { + // Repeat the loop certificate = certificate.issuerCertificate; } } - this.logger.debug( - `Peer certificate chain consists of ${certificateChain.length} certificates.` - ); + this.logger.debug(`Peer certificate chain: [${certificateStrings.join(', ')}]`); - return certificateChain; + return { certificateChain, isChainIncomplete }; } } diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap index 75886b4573edd..6c6782f800ca6 100644 --- a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ResetSessionPage renders as expected 1`] = `"MockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; +exports[`ResetSessionPage renders as expected 1`] = `"MockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; diff --git a/x-pack/plugins/security/server/authorization/reset_session_page.tsx b/x-pack/plugins/security/server/authorization/reset_session_page.tsx index 5ab6fe941ae19..196191df4b655 100644 --- a/x-pack/plugins/security/server/authorization/reset_session_page.tsx +++ b/x-pack/plugins/security/server/authorization/reset_session_page.tsx @@ -46,37 +46,15 @@ export function ResetSessionPage({ ))} - - - - - - -