diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 8a972c65f8412..3ca6b6a4b9098 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=14.15.3 +ARG NODE_VERSION=14.15.4 FROM node:${NODE_VERSION} AS base diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index fd620d31a1e64..0e23064385a63 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -15,6 +15,7 @@ readonly links: { readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; + readonly discover: Record; readonly filebeat: { readonly base: string; readonly installation: string; 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/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/packages/elastic-datemath/__tests__/index.js b/packages/elastic-datemath/index.test.js similarity index 69% rename from packages/elastic-datemath/__tests__/index.js rename to packages/elastic-datemath/index.test.js index 1a61021b48a6e..a6a576dd713a9 100644 --- a/packages/elastic-datemath/__tests__/index.js +++ b/packages/elastic-datemath/index.test.js @@ -17,22 +17,21 @@ * under the License. */ -const dateMath = require('../index'); +const dateMath = require('./index'); const moment = require('moment'); -const sinon = require('sinon'); -const expect = require('@kbn/expect'); /** - * Require a new instance of the moment library, bypassing the require cache. + * Require a new instance of the moment library, bypassing the require cache + * by using jest.resetModules(). * This is needed, since we are trying to test whether or not this library works * when passing in a different configured moment instance. If we would change * the locales on the imported moment, it would automatically apply * to the source code, even without passing it in to the method, since they share * the same global state. This method avoids this, by loading a separate instance - * of moment, by deleting the require cache and require the library again. + * of moment, by resetting the jest require modules cache and require the library again. */ function momentClone() { - delete require.cache[require.resolve('moment')]; + jest.resetModules(); return require('moment'); } @@ -43,44 +42,43 @@ describe('dateMath', function () { const anchoredDate = new Date(Date.parse(anchor)); const unix = moment(anchor).valueOf(); const format = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; - let clock; describe('errors', function () { it('should return undefined if passed something falsy', function () { - expect(dateMath.parse()).to.be(undefined); + expect(dateMath.parse()).toBeUndefined(); }); it('should return undefined if I pass an operator besides [+-/]', function () { - expect(dateMath.parse('now&1d')).to.be(undefined); + expect(dateMath.parse('now&1d')).toBeUndefined(); }); it('should return undefined if I pass a unit besides' + spans.toString(), function () { - expect(dateMath.parse('now+5f')).to.be(undefined); + expect(dateMath.parse('now+5f')).toBeUndefined(); }); it('should return undefined if rounding unit is not 1', function () { - expect(dateMath.parse('now/2y')).to.be(undefined); - expect(dateMath.parse('now/0.5y')).to.be(undefined); + expect(dateMath.parse('now/2y')).toBeUndefined(); + expect(dateMath.parse('now/0.5y')).toBeUndefined(); }); it('should not go into an infinite loop when missing a unit', function () { - expect(dateMath.parse('now-0')).to.be(undefined); - expect(dateMath.parse('now-00')).to.be(undefined); - expect(dateMath.parse('now-000')).to.be(undefined); + expect(dateMath.parse('now-0')).toBeUndefined(); + expect(dateMath.parse('now-00')).toBeUndefined(); + expect(dateMath.parse('now-000')).toBeUndefined(); }); describe('forceNow', function () { it('should throw an Error if passed a string', function () { const fn = () => dateMath.parse('now', { forceNow: '2000-01-01T00:00:00.000Z' }); - expect(fn).to.throwError(); + expect(fn).toThrowError(); }); it('should throw an Error if passed a moment', function () { - expect(() => dateMath.parse('now', { forceNow: moment() })).to.throwError(); + expect(() => dateMath.parse('now', { forceNow: moment() })).toThrowError(); }); it('should throw an Error if passed an invalid date', function () { - expect(() => dateMath.parse('now', { forceNow: new Date('foobar') })).to.throwError(); + expect(() => dateMath.parse('now', { forceNow: new Date('foobar') })).toThrowError(); }); }); }); @@ -92,7 +90,8 @@ describe('dateMath', function () { let now; beforeEach(function () { - clock = sinon.useFakeTimers(unix); + jest.useFakeTimers('modern'); + jest.setSystemTime(unix); now = moment(); mmnt = moment(anchor); date = mmnt.toDate(); @@ -100,27 +99,27 @@ describe('dateMath', function () { }); afterEach(function () { - clock.restore(); + jest.useRealTimers(); }); it('should return the same moment if passed a moment', function () { - expect(dateMath.parse(mmnt)).to.eql(mmnt); + expect(dateMath.parse(mmnt)).toEqual(mmnt); }); it('should return a moment if passed a date', function () { - expect(dateMath.parse(date).format(format)).to.eql(mmnt.format(format)); + expect(dateMath.parse(date).format(format)).toEqual(mmnt.format(format)); }); it('should return a moment if passed an ISO8601 string', function () { - expect(dateMath.parse(string).format(format)).to.eql(mmnt.format(format)); + expect(dateMath.parse(string).format(format)).toEqual(mmnt.format(format)); }); it('should return the current time when parsing now', function () { - expect(dateMath.parse('now').format(format)).to.eql(now.format(format)); + expect(dateMath.parse('now').format(format)).toEqual(now.format(format)); }); it('should use the forceNow parameter when parsing now', function () { - expect(dateMath.parse('now', { forceNow: anchoredDate }).valueOf()).to.eql(unix); + expect(dateMath.parse('now', { forceNow: anchoredDate }).valueOf()).toEqual(unix); }); }); @@ -129,13 +128,14 @@ describe('dateMath', function () { let anchored; beforeEach(function () { - clock = sinon.useFakeTimers(unix); + jest.useFakeTimers('modern'); + jest.setSystemTime(unix); now = moment(); anchored = moment(anchor); }); afterEach(function () { - clock.restore(); + jest.useRealTimers(); }); [5, 12, 247].forEach((len) => { @@ -145,17 +145,17 @@ describe('dateMath', function () { it('should return ' + len + span + ' ago', function () { const parsed = dateMath.parse(nowEx).format(format); - expect(parsed).to.eql(now.subtract(len, span).format(format)); + expect(parsed).toEqual(now.subtract(len, span).format(format)); }); it('should return ' + len + span + ' before ' + anchor, function () { const parsed = dateMath.parse(thenEx).format(format); - expect(parsed).to.eql(anchored.subtract(len, span).format(format)); + expect(parsed).toEqual(anchored.subtract(len, span).format(format)); }); it('should return ' + len + span + ' before forceNow', function () { const parsed = dateMath.parse(nowEx, { forceNow: anchoredDate }).valueOf(); - expect(parsed).to.eql(anchored.subtract(len, span).valueOf()); + expect(parsed).toEqual(anchored.subtract(len, span).valueOf()); }); }); }); @@ -166,13 +166,14 @@ describe('dateMath', function () { let anchored; beforeEach(function () { - clock = sinon.useFakeTimers(unix); + jest.useFakeTimers('modern'); + jest.setSystemTime(unix); now = moment(); anchored = moment(anchor); }); afterEach(function () { - clock.restore(); + jest.useRealTimers(); }); [5, 12, 247].forEach((len) => { @@ -181,17 +182,17 @@ describe('dateMath', function () { const thenEx = `${anchor}||+${len}${span}`; it('should return ' + len + span + ' from now', function () { - expect(dateMath.parse(nowEx).format(format)).to.eql(now.add(len, span).format(format)); + expect(dateMath.parse(nowEx).format(format)).toEqual(now.add(len, span).format(format)); }); it('should return ' + len + span + ' after ' + anchor, function () { - expect(dateMath.parse(thenEx).format(format)).to.eql( + expect(dateMath.parse(thenEx).format(format)).toEqual( anchored.add(len, span).format(format) ); }); it('should return ' + len + span + ' after forceNow', function () { - expect(dateMath.parse(nowEx, { forceNow: anchoredDate }).valueOf()).to.eql( + expect(dateMath.parse(nowEx, { forceNow: anchoredDate }).valueOf()).toEqual( anchored.add(len, span).valueOf() ); }); @@ -204,30 +205,31 @@ describe('dateMath', function () { let anchored; beforeEach(function () { - clock = sinon.useFakeTimers(unix); + jest.useFakeTimers('modern'); + jest.setSystemTime(unix); now = moment(); anchored = moment(anchor); }); afterEach(function () { - clock.restore(); + jest.useRealTimers(); }); spans.forEach((span) => { it(`should round now to the beginning of the ${span}`, function () { - expect(dateMath.parse('now/' + span).format(format)).to.eql( + expect(dateMath.parse('now/' + span).format(format)).toEqual( now.startOf(span).format(format) ); }); it(`should round now to the beginning of forceNow's ${span}`, function () { - expect(dateMath.parse('now/' + span, { forceNow: anchoredDate }).valueOf()).to.eql( + expect(dateMath.parse('now/' + span, { forceNow: anchoredDate }).valueOf()).toEqual( anchored.startOf(span).valueOf() ); }); it(`should round now to the end of the ${span}`, function () { - expect(dateMath.parse('now/' + span, { roundUp: true }).format(format)).to.eql( + expect(dateMath.parse('now/' + span, { roundUp: true }).format(format)).toEqual( now.endOf(span).format(format) ); }); @@ -235,7 +237,7 @@ describe('dateMath', function () { it(`should round now to the end of forceNow's ${span}`, function () { expect( dateMath.parse('now/' + span, { roundUp: true, forceNow: anchoredDate }).valueOf() - ).to.eql(anchored.endOf(span).valueOf()); + ).toEqual(anchored.endOf(span).valueOf()); }); }); }); @@ -245,38 +247,39 @@ describe('dateMath', function () { let anchored; beforeEach(function () { - clock = sinon.useFakeTimers(unix); + jest.useFakeTimers('modern'); + jest.setSystemTime(unix); now = moment(); anchored = moment(anchor); }); afterEach(function () { - clock.restore(); + jest.useRealTimers(); }); it('should round to the nearest second with 0 value', function () { const val = dateMath.parse('now-0s/s').format(format); - expect(val).to.eql(now.startOf('s').format(format)); + expect(val).toEqual(now.startOf('s').format(format)); }); it('should subtract 17s, rounded to the nearest second', function () { const val = dateMath.parse('now-17s/s').format(format); - expect(val).to.eql(now.startOf('s').subtract(17, 's').format(format)); + expect(val).toEqual(now.startOf('s').subtract(17, 's').format(format)); }); it('should add 555ms, rounded to the nearest millisecond', function () { const val = dateMath.parse('now+555ms/ms').format(format); - expect(val).to.eql(now.add(555, 'ms').startOf('ms').format(format)); + expect(val).toEqual(now.add(555, 'ms').startOf('ms').format(format)); }); it('should subtract 555ms, rounded to the nearest second', function () { const val = dateMath.parse('now-555ms/s').format(format); - expect(val).to.eql(now.subtract(555, 'ms').startOf('s').format(format)); + expect(val).toEqual(now.subtract(555, 'ms').startOf('s').format(format)); }); it('should round weeks to Sunday by default', function () { const val = dateMath.parse('now-1w/w'); - expect(val.isoWeekday()).to.eql(7); + expect(val.isoWeekday()).toEqual(7); }); it('should round weeks based on the passed moment locale start of week setting', function () { @@ -286,7 +289,7 @@ describe('dateMath', function () { week: { dow: 2 }, }); const val = dateMath.parse('now-1w/w', { momentInstance: m }); - expect(val.isoWeekday()).to.eql(2); + expect(val.isoWeekday()).toEqual(2); }); it('should round up weeks based on the passed moment locale start of week setting', function () { @@ -301,79 +304,79 @@ describe('dateMath', function () { }); // The end of the range (rounding up) should be the last day of the week (so one day before) // our start of the week, that's why 3 - 1 - expect(val.isoWeekday()).to.eql(3 - 1); + expect(val.isoWeekday()).toEqual(3 - 1); }); it('should round relative to forceNow', function () { const val = dateMath.parse('now-0s/s', { forceNow: anchoredDate }).valueOf(); - expect(val).to.eql(anchored.startOf('s').valueOf()); + expect(val).toEqual(anchored.startOf('s').valueOf()); }); it('should parse long expressions', () => { - expect(dateMath.parse('now-1d/d+8h+50m')).to.be.ok(); + expect(dateMath.parse('now-1d/d+8h+50m')).toBeTruthy(); }); }); describe('used momentjs instance', function () { it('should use the default moment instance if parameter not specified', function () { - const momentSpy = sinon.spy(moment, 'isMoment'); + const momentSpy = jest.spyOn(moment, 'isMoment'); dateMath.parse('now'); - expect(momentSpy.called).to.be(true); - momentSpy.restore(); + expect(momentSpy).toHaveBeenCalled(); + momentSpy.mockRestore(); }); it('should not use default moment instance if parameter is specified', function () { const m = momentClone(); - const momentSpy = sinon.spy(moment, 'isMoment'); - const cloneSpy = sinon.spy(m, 'isMoment'); + const momentSpy = jest.spyOn(moment, 'isMoment'); + const cloneSpy = jest.spyOn(m, 'isMoment'); dateMath.parse('now', { momentInstance: m }); - expect(momentSpy.called).to.be(false); - expect(cloneSpy.called).to.be(true); - momentSpy.restore(); - cloneSpy.restore(); + expect(momentSpy).not.toHaveBeenCalled(); + expect(cloneSpy).toHaveBeenCalled(); + momentSpy.mockRestore(); + cloneSpy.mockRestore(); }); it('should work with multiple different instances', function () { const m1 = momentClone(); const m2 = momentClone(); - const m1Spy = sinon.spy(m1, 'isMoment'); - const m2Spy = sinon.spy(m2, 'isMoment'); + const m1Spy = jest.spyOn(m1, 'isMoment'); + const m2Spy = jest.spyOn(m2, 'isMoment'); dateMath.parse('now', { momentInstance: m1 }); - expect(m1Spy.called).to.be(true); - expect(m2Spy.called).to.be(false); - m1Spy.resetHistory(); - m2Spy.resetHistory(); + expect(m1Spy).toHaveBeenCalled(); + expect(m2Spy).not.toHaveBeenCalled(); + m1Spy.mockClear(); + m2Spy.mockClear(); dateMath.parse('now', { momentInstance: m2 }); - expect(m1Spy.called).to.be(false); - expect(m2Spy.called).to.be(true); - m1Spy.restore(); - m2Spy.restore(); + expect(m1Spy).not.toHaveBeenCalled(); + expect(m2Spy).toHaveBeenCalled(); + m1Spy.mockRestore(); + m2Spy.mockRestore(); }); it('should use global instance after passing an instance', function () { const m = momentClone(); - const momentSpy = sinon.spy(moment, 'isMoment'); - const cloneSpy = sinon.spy(m, 'isMoment'); + const momentSpy = jest.spyOn(moment, 'isMoment'); + const cloneSpy = jest.spyOn(m, 'isMoment'); dateMath.parse('now', { momentInstance: m }); - expect(momentSpy.called).to.be(false); - expect(cloneSpy.called).to.be(true); - momentSpy.resetHistory(); - cloneSpy.resetHistory(); + expect(momentSpy).not.toHaveBeenCalled(); + expect(cloneSpy).toHaveBeenCalled(); + momentSpy.mockClear(); + cloneSpy.mockClear(); dateMath.parse('now'); - expect(momentSpy.called).to.be(true); - expect(cloneSpy.called).to.be(false); - momentSpy.restore(); - cloneSpy.restore(); + expect(momentSpy).toHaveBeenCalled(); + expect(cloneSpy).not.toHaveBeenCalled(); + momentSpy.mockRestore(); + cloneSpy.mockRestore(); }); }); describe('units', function () { it('should have units descending for unitsDesc', function () { - expect(dateMath.unitsDesc).to.eql(['y', 'M', 'w', 'd', 'h', 'm', 's', 'ms']); + expect(dateMath.unitsDesc).toEqual(['y', 'M', 'w', 'd', 'h', 'm', 's', 'ms']); }); it('should have units ascending for unitsAsc', function () { - expect(dateMath.unitsAsc).to.eql(['ms', 's', 'm', 'h', 'd', 'w', 'M', 'y']); + expect(dateMath.unitsAsc).toEqual(['ms', 's', 'm', 'h', 'd', 'w', 'M', 'y']); }); }); }); diff --git a/packages/elastic-datemath/jest.config.js b/packages/elastic-datemath/jest.config.js new file mode 100644 index 0000000000000..6a0bd891f1c58 --- /dev/null +++ b/packages/elastic-datemath/jest.config.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/elastic-datemath'], + testEnvironment: 'jsdom', +}; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index a9e55fdc22dc6..927f94c10fc42 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -43,6 +43,9 @@ export class DocLinksService { urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html`, urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#url-template-variables`, }, + discover: { + guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/discover.html`, + }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, installation: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-installation-configuration.html`, @@ -243,6 +246,7 @@ export interface DocLinksStart { readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; + readonly discover: Record; readonly filebeat: { readonly base: string; readonly installation: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 26ce358cc9ade..dd06022dc4831 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -494,6 +494,7 @@ export interface DocLinksStart { readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; + readonly discover: Record; readonly filebeat: { readonly base: string; readonly installation: string; 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/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/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/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 3e440c89b82d8..6822fcddc3ac5 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -169,6 +169,7 @@ kibana_vars=( xpack.fleet.agents.elasticsearch.host xpack.fleet.agents.kibana.host xpack.fleet.agents.tlsCheckDisabled + xpack.fleet.registryUrl xpack.graph.enabled xpack.graph.canEditDrillDownUrls xpack.graph.savePolicy 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/lib/help_menu_util.ts b/src/plugins/dashboard/public/application/lib/help_menu_util.ts index efdff051a25a0..ee06e7bc5ecf2 100644 --- a/src/plugins/dashboard/public/application/lib/help_menu_util.ts +++ b/src/plugins/dashboard/public/application/lib/help_menu_util.ts @@ -31,7 +31,7 @@ export function addHelpMenuToAppChrome( links: [ { linkType: 'documentation', - href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/dashboard.html`, + href: `${docLinks.links.dashboard.guide}`, }, ], }); 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/server/server.api.md b/src/plugins/data/server/server.api.md index bb8ed92e9bfce..c58cd11bbc5bc 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -38,7 +38,6 @@ import { IUiSettingsClient } from 'src/core/server'; import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/server'; import { KibanaRequest } from 'kibana/server'; import { KibanaRequest as KibanaRequest_2 } from 'src/core/server'; -import { LegacyAPICaller } from 'src/core/server'; import { Logger } from 'src/core/server'; import { Logger as Logger_2 } from 'kibana/server'; import { LoggerFactory } from '@kbn/logging'; diff --git a/src/plugins/discover/public/application/components/help_menu/help_menu_util.js b/src/plugins/discover/public/application/components/help_menu/help_menu_util.js index 03ab44966f796..e3c2adf587d91 100644 --- a/src/plugins/discover/public/application/components/help_menu/help_menu_util.js +++ b/src/plugins/discover/public/application/components/help_menu/help_menu_util.js @@ -29,7 +29,7 @@ export function addHelpMenuToAppChrome(chrome) { links: [ { linkType: 'documentation', - href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/discover.html`, + href: `${docLinks.links.discover.guide}`, }, ], }); 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/src/plugins/telemetry/server/fetcher.ts b/src/plugins/telemetry/server/fetcher.ts index 820f2c7c4c4af..23b4b0640e978 100644 --- a/src/plugins/telemetry/server/fetcher.ts +++ b/src/plugins/telemetry/server/fetcher.ts @@ -31,7 +31,7 @@ import { SavedObjectsClientContract, SavedObjectsClient, CoreStart, - ILegacyCustomClusterClient, + ICustomClusterClient, } from '../../../core/server'; import { getTelemetryOptIn, @@ -65,7 +65,7 @@ export class FetcherTask { private isSending = false; private internalRepository?: SavedObjectsClientContract; private telemetryCollectionManager?: TelemetryCollectionManagerPluginStart; - private elasticsearchClient?: ILegacyCustomClusterClient; + private elasticsearchClient?: ICustomClusterClient; constructor(initializerContext: PluginInitializerContext) { this.config$ = initializerContext.config.create(); @@ -79,7 +79,7 @@ export class FetcherTask { ) { this.internalRepository = new SavedObjectsClient(savedObjects.createInternalRepository()); this.telemetryCollectionManager = telemetryCollectionManager; - this.elasticsearchClient = elasticsearch.legacy.createClient('telemetry-fetcher'); + this.elasticsearchClient = elasticsearch.createClient('telemetry-fetcher'); this.intervalId = timer(this.initialCheckDelayMs, this.checkIntervalMs).subscribe(() => this.sendIfDue() diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index f40c38b2cbbd0..95b44ae560f20 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -99,7 +99,7 @@ export class TelemetryPlugin implements Plugin { - const usage = await usageCollection.bulkFetch( - callWithInternalUser, - asInternalUser, - soClient, - kibanaRequest - ); + const usage = await usageCollection.bulkFetch(asInternalUser, soClient, kibanaRequest); return usageCollection.toObject(usage); } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index 12245ce62305e..866aed520bd2e 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -71,7 +71,7 @@ export const getLocalStats: StatsGetter = async ( config, context ) => { - const { callCluster, usageCollection, esClient, soClient, kibanaRequest } = config; + const { usageCollection, esClient, soClient, kibanaRequest } = config; return await Promise.all( clustersDetails.map(async (clustersDetail) => { @@ -79,7 +79,7 @@ export const getLocalStats: StatsGetter = async ( getClusterInfo(esClient), // cluster info getClusterStats(esClient), // cluster stats (not to be confused with cluster _state_) getNodesUsage(esClient), // nodes_usage info - getKibana(usageCollection, callCluster, esClient, soClient, kibanaRequest), + getKibana(usageCollection, esClient, soClient, kibanaRequest), getDataTelemetry(esClient), ]); return handleLocalStats( diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index a135f4b115b21..247d9ce2c366c 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -26,7 +26,6 @@ import { Logger, IClusterClient, SavedObjectsServiceStart, - ILegacyClusterClient, } from 'src/core/server'; import { @@ -53,7 +52,6 @@ export class TelemetryCollectionManagerPlugin private collectionStrategy: CollectionStrategy | undefined; private usageGetterMethodPriority = -1; private usageCollection?: UsageCollectionSetup; - private legacyElasticsearchClient?: ILegacyClusterClient; private elasticsearchClient?: IClusterClient; private savedObjectsService?: SavedObjectsServiceStart; private readonly isDistributable: boolean; @@ -77,7 +75,6 @@ export class TelemetryCollectionManagerPlugin } public start(core: CoreStart) { - this.legacyElasticsearchClient = core.elasticsearch.legacy.client; // TODO: Remove when all the collectors have migrated this.elasticsearchClient = core.elasticsearch.client; this.savedObjectsService = core.savedObjects; @@ -129,9 +126,6 @@ export class TelemetryCollectionManagerPlugin config: StatsGetterConfig, usageCollection: UsageCollectionSetup ): StatsCollectionConfig | undefined { - const callCluster = config.unencrypted - ? this.legacyElasticsearchClient?.asScoped(config.request).callAsCurrentUser - : this.legacyElasticsearchClient?.callAsInternalUser; // Scope the new elasticsearch Client appropriately and pass to the stats collection config const esClient = config.unencrypted ? this.elasticsearchClient?.asScoped(config.request).asCurrentUser @@ -143,8 +137,8 @@ export class TelemetryCollectionManagerPlugin // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted const kibanaRequest = config.unencrypted ? config.request : void 0; - if (callCluster && esClient && soClient) { - return { callCluster, usageCollection, esClient, soClient, kibanaRequest }; + if (esClient && soClient) { + return { usageCollection, esClient, soClient, kibanaRequest }; } } diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 05641d5064593..49e217b9e3d75 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -18,7 +18,6 @@ */ import { - LegacyAPICaller, ElasticsearchClient, Logger, KibanaRequest, @@ -68,7 +67,6 @@ export interface ClusterDetails { export interface StatsCollectionConfig { usageCollection: UsageCollectionSetup; - callCluster: LegacyAPICaller; esClient: ElasticsearchClient; soClient: SavedObjectsClientContract | ISavedObjectsRepository; kibanaRequest: KibanaRequest | undefined; // intentionally `| undefined` to enforce providing the parameter diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 85c910cd09bf1..76404bf6329dd 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -95,10 +95,10 @@ Some background: - `isReady` (added in v7.2.0 and v6.8.4) is a way for a usage collector to announce that some async process must finish first before it can return data in the `fetch` method (e.g. a client needs to ne initialized, or the task manager needs to run a task first). If any collector reports that it is not ready when we call its `fetch` method, we reset a flag to try again and, after a set amount of time, collect data from those collectors that are ready and skip any that are not. This means that if a collector returns `true` for `isReady` and it actually isn't ready to return data, there won't be telemetry data from that collector in that telemetry report (usually once per day). You should consider what it means if your collector doesn't return data in the first few documents when Kibana starts or, if we should wait for any other reason (e.g. the task manager needs to run your task first). If you need to tell telemetry collection to wait, you should implement this function with custom logic. If your `fetch` method can run without the need of any previous dependencies, then you can return true for `isReady` as shown in the example below. -- The `fetch` method needs to support multiple contexts in which it is called. For example, when a user requests the example of what we collect in the **Kibana>Advanced Settings>Usage data** section, the clients provided in the context of the function (`CollectorFetchContext`) are scoped to that user's privileges. The reason is to avoid exposing via telemetry any data that user should not have access to (i.e.: if the user does not have access to certain indices, they shouldn't be allowed to see the number of documents that exists in it). In this case, the `fetch` method receives the clients `callCluster`, `esClient` and `soClient` scoped to the user who performed the HTTP API request. Alternatively, when requesting the usage data to be reported to the Remote Telemetry Service, the clients are scoped to the internal Kibana user (`kibana_system`). Please, mind it might have lower-level access than the default super-admin `elastic` test user. +- The `fetch` method needs to support multiple contexts in which it is called. For example, when a user requests the example of what we collect in the **Kibana>Advanced Settings>Usage data** section, the clients provided in the context of the function (`CollectorFetchContext`) are scoped to that user's privileges. The reason is to avoid exposing via telemetry any data that user should not have access to (i.e.: if the user does not have access to certain indices, they shouldn't be allowed to see the number of documents that exists in it). In this case, the `fetch` method receives the clients `esClient` and `soClient` scoped to the user who performed the HTTP API request. Alternatively, when requesting the usage data to be reported to the Remote Telemetry Service, the clients are scoped to the internal Kibana user (`kibana_system`). Please, mind it might have lower-level access than the default super-admin `elastic` test user. In some scenarios, your collector might need to maintain its own client. An example of that is the `monitoring` plugin, that maintains a connection to the Remote Monitoring Cluster to push its monitoring data. If that's the case, your plugin can opt-in to receive the additional `kibanaRequest` parameter by adding `extendFetchContext.kibanaRequest: true` to the collector's config: it will be appended to the context of the `fetch` method only if the request needs to be scoped to a user other than Kibana Internal, so beware that your collector will need to work for both scenarios (especially for the scenario when `kibanaRequest` is missing). -Note: there will be many cases where you won't need to use the `callCluster`, `esClient` or `soClient` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. +Note: there will be many cases where you won't need to use the `esClient` or `soClient` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. In the case of using a custom SavedObjects client, it is up to the plugin to initialize the client to save the data and it is strongly recommended to scope that client to the `kibana_system` user. @@ -302,7 +302,7 @@ New fields added to the telemetry payload currently mean that telemetry cluster There are a few ways you can test that your usage collector is working properly. -1. The `/api/stats?extended=true&legacy=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`. +1. The `/api/stats?extended=true&legacy=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where the elasticsearch client wraps the call with the request. 2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from: - The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes. - Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data. diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 8e86bc3d1cd26..afd5b5883ff17 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -19,7 +19,6 @@ import { Logger, - LegacyAPICaller, ElasticsearchClient, ISavedObjectsRepository, SavedObjectsClientContract, @@ -54,10 +53,6 @@ export type MakeSchemaFrom = { * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster. */ export type CollectorFetchContext = { - /** - * @deprecated Scoped Legacy Elasticsearch client: use esClient instead - */ - callCluster: LegacyAPICaller; /** * Request-scoped Elasticsearch client * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index 90a69043e0635..310714cc2a48a 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -44,7 +44,6 @@ describe('CollectorSet', () => { loggerSpies.debug.mockRestore(); loggerSpies.warn.mockRestore(); }); - const mockCallCluster = jest.fn().mockResolvedValue({ passTest: 1000 }); const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const mockSoClient = savedObjectsRepositoryMock.create(); const req = void 0; // No need to instantiate any KibanaRequest in these tests @@ -83,18 +82,19 @@ describe('CollectorSet', () => { }); it('should log debug status of fetching from the collector', async () => { + mockEsClient.get.mockResolvedValue({ passTest: 1000 } as any); const collectors = new CollectorSet({ logger }); collectors.registerCollector( new Collector(logger, { type: 'MY_TEST_COLLECTOR', fetch: (collectorFetchContext: any) => { - return collectorFetchContext.callCluster(); + return collectorFetchContext.esClient.get(); }, isReady: () => true, }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); expect(loggerSpies.debug).toHaveBeenCalledTimes(1); expect(loggerSpies.debug).toHaveBeenCalledWith( 'Fetching data from MY_TEST_COLLECTOR collector' @@ -119,7 +119,7 @@ describe('CollectorSet', () => { let result; try { - result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); + result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); } catch (err) { // Do nothing } @@ -137,7 +137,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -155,7 +155,7 @@ describe('CollectorSet', () => { } as any) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 3555b05518fdb..fc47bbcd16649 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -20,7 +20,6 @@ import { snakeCase } from 'lodash'; import { Logger, - LegacyAPICaller, ElasticsearchClient, ISavedObjectsRepository, SavedObjectsClientContract, @@ -171,7 +170,6 @@ export class CollectorSet { }; public bulkFetch = async ( - callCluster: LegacyAPICaller, esClient: ElasticsearchClient, soClient: SavedObjectsClientContract | ISavedObjectsRepository, kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter @@ -182,7 +180,6 @@ export class CollectorSet { this.logger.debug(`Fetching data from ${collector.type} collector`); try { const context = { - callCluster, esClient, soClient, ...(collector.extendFetchContext.kibanaRequest && { kibanaRequest }), @@ -212,14 +209,12 @@ export class CollectorSet { }; public bulkFetchUsage = async ( - callCluster: LegacyAPICaller, esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository, kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter ) => { const usageCollectors = this.getFilteredCollectorSet((c) => c instanceof UsageCollector); return await this.bulkFetch( - callCluster, esClient, savedObjectsClient, kibanaRequest, diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index 16a1c2c742f04..bbb4c94e02d5f 100644 --- a/src/plugins/usage_collection/server/routes/stats/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -28,7 +28,6 @@ import { IRouter, ISavedObjectsRepository, KibanaRequest, - LegacyAPICaller, MetricsServiceSetup, SavedObjectsClientContract, ServiceStatus, @@ -66,17 +65,11 @@ export function registerStatsRoute({ overallStatus$: Observable; }) { const getUsage = async ( - callCluster: LegacyAPICaller, esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository, kibanaRequest: KibanaRequest ): Promise => { - const usage = await collectorSet.bulkFetchUsage( - callCluster, - esClient, - savedObjectsClient, - kibanaRequest - ); + const usage = await collectorSet.bulkFetchUsage(esClient, savedObjectsClient, kibanaRequest); return collectorSet.toObject(usage); }; @@ -110,7 +103,6 @@ export function registerStatsRoute({ let extended; if (isExtended) { - const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const { asCurrentUser } = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; @@ -122,7 +114,7 @@ export function registerStatsRoute({ } const usagePromise = shouldGetUsage - ? getUsage(callCluster, asCurrentUser, savedObjectsClient, req) + ? getUsage(asCurrentUser, savedObjectsClient, req) : Promise.resolve({}); const [usage, clusterUuid] = await Promise.all([ usagePromise, diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts index fb0a2e56ff3c9..1295572335a66 100644 --- a/src/plugins/usage_collection/server/usage_collection.mock.ts +++ b/src/plugins/usage_collection/server/usage_collection.mock.ts @@ -50,7 +50,6 @@ export const createUsageCollectionSetupMock = () => { export function createCollectorFetchContextMock(): jest.Mocked> { const collectorFetchClientsMock: jest.Mocked> = { - callCluster: elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser, esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, soClient: savedObjectsRepositoryMock.create(), }; @@ -61,7 +60,6 @@ export function createCollectorFetchContextWithKibanaMock(): jest.Mocked< CollectorFetchContext > { const collectorFetchClientsMock: jest.Mocked> = { - callCluster: elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser, esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, soClient: savedObjectsRepositoryMock.create(), kibanaRequest: httpServerMock.createKibanaRequest(), diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 662350bfc3bd0..3ddf0757ba4c8 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -57,5 +57,5 @@ export { VisToExpressionAst, } from './types'; export { ExprVisAPIEvents } from './expressions/vis'; -export { VisualizationListItem } from './vis_types/vis_type_alias_registry'; +export { VisualizationListItem, VisualizationStage } from './vis_types/vis_type_alias_registry'; export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index c05f42d684dbe..c16ddf436381d 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -16,6 +16,9 @@ * specific language governing permissions and limitations * under the License. */ +import { SavedObject } from '../../../../core/types/saved_objects'; + +export type VisualizationStage = 'experimental' | 'beta' | 'production'; export interface VisualizationListItem { editUrl: string; @@ -23,7 +26,7 @@ export interface VisualizationListItem { error?: string; icon: string; id: string; - stage: 'experimental' | 'beta' | 'production'; + stage: VisualizationStage; savedObjectType: string; title: string; description?: string; @@ -35,11 +38,7 @@ export interface VisualizationListItem { export interface VisualizationsAppExtension { docTypes: string[]; searchFields?: string[]; - toListItem: (savedObject: { - id: string; - type: string; - attributes: object; - }) => VisualizationListItem; + toListItem: (savedObject: SavedObject) => VisualizationListItem; } export interface VisTypeAliasPromoTooltip { @@ -59,7 +58,7 @@ export interface VisTypeAlias { note?: string; disabled?: boolean; getSupportedTriggers?: () => string[]; - stage: 'experimental' | 'beta' | 'production'; + stage: VisualizationStage; appExtensions?: { visualizations: VisualizationsAppExtension; 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/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/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 ( <> - - - - - +
{ let lib: ConfigBlocksLib; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts index e8944b15978a9..57f4a9f66b987 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -10,7 +10,13 @@ export { mockLicensingValues } from './licensing_logic.mock'; export { mockHttpValues } from './http_logic.mock'; export { mockTelemetryActions } from './telemetry_logic.mock'; export { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; -export { mockAllValues, mockAllActions, setMockValues, setMockActions } from './kea.mock'; +export { + mockAllValues, + mockAllActions, + setMockValues, + setMockActions, + LogicMounter, +} from './kea.mock'; export { mountAsync } from './mount_async.mock'; export { mountWithIntl } from './mount_with_i18n.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts index 0176f8c03c632..78ffbcfa3526f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -40,14 +40,18 @@ jest.mock('kea', () => ({ })); /** + * React component helpers + * * Call this function to override a specific set of Kea values while retaining all other defaults - * Example usage within a component test: * - * import '../../../__mocks__/kea'; - * import { setMockValues } from ''../../../__mocks__'; + * Example usage: + * + * import { setMockValues } from '../../../__mocks__/kea.mock'; + * import { SomeComponent } from './'; * * it('some test', () => { * setMockValues({ someValue: 'hello' }); + * shallow(); * }); */ import { useValues, useActions } from 'kea'; @@ -58,3 +62,62 @@ export const setMockValues = (values: object) => { export const setMockActions = (actions: object) => { (useActions as jest.Mock).mockImplementation(() => ({ ...mockAllActions, ...actions })); }; + +/** + * Kea logic helpers + * + * Call this function to mount a logic file and optionally override default values. + * Automatically DRYs out a lot of cruft for us, such as resetting context, creating the + * nested defaults path obj (see https://kea.js.org/docs/api/context#resetcontext), and + * returning an unmount function + * + * Example usage: + * + * import { LogicMounter } from '../../../__mocks__/kea.mock'; + * import { SomeLogic } from './'; + * + * const { mount, unmount } = new LogicMounter(SomeLogic); + * + * it('some test', () => { + * mount({ someValue: 'hello' }); + * unmount(); + * }); + */ +import { resetContext, Logic, LogicInput } from 'kea'; + +interface LogicFile { + inputs: Array>; + mount(): Function; +} +export class LogicMounter { + private logicFile: LogicFile; + private unmountFn!: Function; + + constructor(logicFile: LogicFile) { + this.logicFile = logicFile; + } + + // Reset context with optional default value overrides + public resetContext = (values?: object) => { + if (!values) { + resetContext({}); + } else { + const path = this.logicFile.inputs[0].path as string[]; // example: ['x', 'y', 'z'] + const defaults = path.reduceRight((value: object, key: string) => ({ [key]: value }), values); // example: { x: { y: { z: values } } } + resetContext({ defaults }); + } + }; + + // Automatically reset context & mount the logic file + public mount = (values?: object) => { + this.resetContext(values); + const unmount = this.logicFile.mount(); + this.unmountFn = unmount; + return unmount; // Keep Kea behavior of returning an unmount fn from mount + }; + + // Also add unmount as a class method that can be destructured on init without becoming stale later + public unmount = () => { + this.unmountFn(); + }; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index 6523b4fb110b0..48be2b0ae8dfd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; import { mockHttpValues } from '../../../__mocks__'; jest.mock('../../../shared/http', () => ({ @@ -57,24 +57,7 @@ describe('CredentialsLogic', () => { fullEngineAccessChecked: false, }; - const mount = (defaults?: object) => { - if (!defaults) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - credentials_logic: { - ...defaults, - }, - }, - }, - }, - }); - } - CredentialsLogic.mount(); - }; + const { mount } = new LogicMounter(CredentialsLogic); const newToken = { id: 1, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts index bb0103b07b072..c2a0d29cc1f40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; + import dedent from 'dedent'; jest.mock('./utils', () => ({ @@ -39,24 +40,7 @@ describe('DocumentCreationLogic', () => { }; const mockFile = new File(['mockFile'], 'mockFile.json'); - const mount = (defaults?: object) => { - if (!defaults) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - document_creation_logic: { - ...defaults, - }, - }, - }, - }, - }); - } - DocumentCreationLogic.mount(); - }; + const { mount } = new LogicMounter(DocumentCreationLogic); beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index d9a3de7c078cc..fe735f70247c6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; import { mockHttpValues } from '../../../__mocks__'; jest.mock('../../../shared/http', () => ({ @@ -36,24 +36,7 @@ describe('DocumentDetailLogic', () => { fields: [], }; - const mount = (defaults?: object) => { - if (!defaults) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - document_detail_logic: { - ...defaults, - }, - }, - }, - }, - }); - } - DocumentDetailLogic.mount(); - }; + const { mount } = new LogicMounter(DocumentDetailLogic); beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts index 236172f0f7bdf..2863a39535ef4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; import { DocumentsLogic } from './documents_logic'; @@ -13,24 +13,7 @@ describe('DocumentsLogic', () => { isDocumentCreationOpen: false, }; - const mount = (defaults?: object) => { - if (!defaults) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - documents_logic: { - ...defaults, - }, - }, - }, - }, - }); - } - DocumentsLogic.mount(); - }; + const { mount } = new LogicMounter(DocumentsLogic); describe('actions', () => { describe('openDocumentCreation', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts index 13db440df739e..094260b6df095 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; import { mockHttpValues } from '../../../__mocks__'; jest.mock('../../../shared/http', () => ({ @@ -46,24 +46,7 @@ describe('EngineLogic', () => { engineNotFound: false, }; - const mount = (values?: object) => { - if (!values) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - engine_logic: { - ...values, - }, - }, - }, - }, - }); - } - EngineLogic.mount(); - }; + const { mount } = new LogicMounter(EngineLogic); beforeEach(() => { jest.clearAllMocks(); @@ -153,6 +136,18 @@ describe('EngineLogic', () => { }); }); + describe('engineName', () => { + it('should be reset to an empty string', () => { + mount({ engineName: 'hello-world' }); + EngineLogic.actions.clearEngine(); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engineName: '', + }); + }); + }); + describe('dataLoading', () => { it('should be set to true', () => { mount({ dataLoading: false }); @@ -164,6 +159,18 @@ describe('EngineLogic', () => { }); }); }); + + describe('engineNotFound', () => { + it('should be set to false', () => { + mount({ engineNotFound: true }); + EngineLogic.actions.clearEngine(); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engineNotFound: false, + }); + }); + }); }); describe('initializeEngine', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts index e1ce7cea0fa91..9f3fe721b74de 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts @@ -64,12 +64,14 @@ export const EngineLogic = kea>({ '', { setEngineName: (_, { engineName }) => engineName, + clearEngine: () => '', }, ], engineNotFound: [ false, { setEngineNotFound: (_, { notFound }) => notFound, + clearEngine: () => false, }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts index d35bde20f4f1e..2063f706a4741 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; import { mockHttpValues } from '../../../__mocks__'; jest.mock('../../../shared/http', () => ({ @@ -48,10 +48,7 @@ describe('EngineOverviewLogic', () => { timeoutId: null, }; - const mount = () => { - resetContext({}); - EngineOverviewLogic.mount(); - }; + const { mount, unmount } = new LogicMounter(EngineOverviewLogic); beforeEach(() => { jest.clearAllMocks(); @@ -141,12 +138,9 @@ describe('EngineOverviewLogic', () => { }); describe('unmount', () => { - let unmount: Function; - beforeEach(() => { jest.useFakeTimers(); - resetContext({}); - unmount = EngineOverviewLogic.mount(); + mount(); }); it('clears existing polling timeouts on unmount', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts index c86d7e3e915e2..8310e2abe045b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resetContext } from 'kea'; +import { LogicMounter } from '../../../../__mocks__/kea.mock'; import { mockHttpValues } from '../../../../__mocks__'; jest.mock('../../../../shared/http', () => ({ @@ -53,24 +53,7 @@ describe('LogRetentionLogic', () => { isLogRetentionUpdating: false, }; - const mount = (defaults?: object) => { - if (!defaults) { - resetContext({}); - } else { - resetContext({ - defaults: { - enterprise_search: { - app_search: { - log_retention_logic: { - ...defaults, - }, - }, - }, - }, - }); - } - LogRetentionLogic.mount(); - }; + const { mount } = new LogicMounter(LogRetentionLogic); beforeEach(() => { jest.clearAllMocks(); 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/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index c267052e6dfe5..6a7448ddc8448 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -28,6 +28,9 @@ export const MAP_SAVED_OBJECT_TYPE = 'map'; export const APP_ID = 'maps'; export const APP_ICON = 'gisApp'; export const APP_ICON_SOLUTION = 'logoKibana'; +export const APP_NAME = i18n.translate('xpack.maps.visTypeAlias.title', { + defaultMessage: 'Maps', +}); export const INITIAL_LAYERS_KEY = 'initialLayers'; export const MAPS_APP_PATH = `app/${APP_ID}`; @@ -50,6 +53,9 @@ export function getNewMapPath() { export function getExistingMapPath(id: string) { return `${MAP_BASE_URL}/${id}`; } +export function getEditPath(id: string) { + return `/${MAP_PATH}/${id}`; +} export enum LAYER_TYPE { TILE = 'TILE', 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={ > = [ { e.preventDefault(); - goToSpecifiedPath(`/${MAP_PATH}/${record.id}`); + goToSpecifiedPath(getEditPath(record.id)); }} data-test-subj={`mapListingTitleLink-${record.title.split(' ').join('-')}`} > diff --git a/x-pack/plugins/maps/public/url_generator.ts b/x-pack/plugins/maps/public/url_generator.ts index 7f7f3f2c60327..be6a7f5fe6fa7 100644 --- a/x-pack/plugins/maps/public/url_generator.ts +++ b/x-pack/plugins/maps/public/url_generator.ts @@ -183,6 +183,7 @@ export const createRegionMapUrlGenerator = ( emsLayerId, leftFieldName, termsFieldName, + termsSize, colorSchema, indexPatternId, indexPatternTitle, @@ -197,6 +198,7 @@ export const createRegionMapUrlGenerator = ( emsLayerId?: string; leftFieldName?: string; termsFieldName?: string; + termsSize?: number; colorSchema: string; indexPatternId?: string; indexPatternTitle?: string; @@ -214,6 +216,7 @@ export const createRegionMapUrlGenerator = ( emsLayerId, leftFieldName, termsFieldName, + termsSize, colorSchema, indexPatternId, indexPatternTitle, diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts index a7f001f166d7d..2aee9bbda77b7 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { SearchResponse } from 'elasticsearch'; -import { StatsCollectionConfig } from 'src/plugins/telemetry_collection_manager/server'; +import { LegacyAPICaller } from 'kibana/server'; import { createQuery } from './create_query'; import { INDEX_PATTERN_BEATS } from '../../common/constants'; @@ -318,7 +318,7 @@ export function processResults( * @return {Promise} */ async function fetchBeatsByType( - callCluster: StatsCollectionConfig['callCluster'], + callCluster: LegacyAPICaller, clusterUuids: string[], start: string, end: string, @@ -382,7 +382,7 @@ async function fetchBeatsByType( } export async function fetchBeatsStats( - callCluster: StatsCollectionConfig['callCluster'], + callCluster: LegacyAPICaller, clusterUuids: string[], start: string, end: string, @@ -392,7 +392,7 @@ export async function fetchBeatsStats( } export async function fetchBeatsStates( - callCluster: StatsCollectionConfig['callCluster'], + callCluster: LegacyAPICaller, clusterUuids: string[], start: string, end: string, @@ -410,7 +410,7 @@ export interface BeatsStatsByClusterUuid { * @return {Object} - Beats stats in an object keyed by the cluster UUIDs */ export async function getBeatsStats( - callCluster: StatsCollectionConfig['callCluster'], + callCluster: LegacyAPICaller, clusterUuids: string[], start: string, end: string diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts index 6325ed0c4b052..455327336462f 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts @@ -5,7 +5,7 @@ */ import { SearchResponse } from 'elasticsearch'; -import { StatsCollectionConfig } from 'src/plugins/telemetry_collection_manager/server'; +import { LegacyAPICaller } from 'kibana/server'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; /** @@ -16,7 +16,7 @@ import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; * @param {Array} clusterUuids The string Cluster UUIDs to fetch details for */ export async function getElasticsearchStats( - callCluster: StatsCollectionConfig['callCluster'], + callCluster: LegacyAPICaller, clusterUuids: string[], maxBucketSize: number ) { @@ -34,7 +34,7 @@ export async function getElasticsearchStats( * Returns the response for the aggregations to fetch details for the product. */ export function fetchElasticsearchStats( - callCluster: StatsCollectionConfig['callCluster'], + callCluster: LegacyAPICaller, clusterUuids: string[], maxBucketSize: number ) { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts index ce1c6ccd4b106..464c3ae406a53 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { SearchResponse } from 'elasticsearch'; -import { StatsCollectionConfig } from 'src/plugins/telemetry_collection_manager/server'; +import { LegacyAPICaller } from 'kibana/server'; import { createQuery } from './create_query'; import { INDEX_PATTERN_KIBANA, @@ -247,7 +247,7 @@ function getIndexPatternForStackProduct(product: string) { * Returns an object keyed by the cluster UUIDs to make grouping easier. */ export async function getHighLevelStats( - callCluster: StatsCollectionConfig['callCluster'], + callCluster: LegacyAPICaller, clusterUuids: string[], start: string, end: string, @@ -268,7 +268,7 @@ export async function getHighLevelStats( export async function fetchHighLevelStats< T extends { cluster_uuid?: string } = { cluster_uuid?: string } >( - callCluster: StatsCollectionConfig['callCluster'], + callCluster: LegacyAPICaller, clusterUuids: string[], start: string, end: string, diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts index 57e75d9c9c12e..cf9ac7768048b 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import { isEmpty } from 'lodash'; import { SearchResponse } from 'elasticsearch'; -import { StatsCollectionConfig } from 'src/plugins/telemetry_collection_manager/server'; +import { LegacyAPICaller } from 'kibana/server'; import { KIBANA_SYSTEM_ID, TELEMETRY_COLLECTION_INTERVAL } from '../../common/constants'; import { fetchHighLevelStats, @@ -182,7 +182,7 @@ export function ensureTimeSpan( * specialized usage data that comes with kibana stats (kibana_stats.usage). */ export async function getKibanaStats( - callCluster: StatsCollectionConfig['callCluster'], + callCluster: LegacyAPICaller, clusterUuids: string[], start: string, end: string, diff --git a/x-pack/plugins/security/common/licensing/index.mock.ts b/x-pack/plugins/security/common/licensing/index.mock.ts index df7d8cd7b416b..88cb3206a253c 100644 --- a/x-pack/plugins/security/common/licensing/index.mock.ts +++ b/x-pack/plugins/security/common/licensing/index.mock.ts @@ -5,14 +5,14 @@ */ import { of } from 'rxjs'; -import { SecurityLicense } from '.'; +import { SecurityLicense, SecurityLicenseFeatures } from '.'; export const licenseMock = { - create: (): jest.Mocked => ({ + create: (features?: Partial): jest.Mocked => ({ isLicenseAvailable: jest.fn(), isEnabled: jest.fn().mockReturnValue(true), getType: jest.fn().mockReturnValue('basic'), getFeatures: jest.fn(), - features$: of(), + features$: features ? of(features as SecurityLicenseFeatures) : of(), }), }; diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index 91c656ad69f18..e828f0135d44b 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -3,7 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AuditService, filterEvent, createLoggingConfig } from './audit_service'; +import { + AuditService, + filterEvent, + createLoggingConfig, + RECORD_USAGE_INTERVAL, +} from './audit_service'; import { AuditEvent, EventCategory, EventType, EventOutcome } from './audit_events'; import { coreMock, @@ -16,6 +21,8 @@ import { ConfigSchema, ConfigType } from '../config'; import { SecurityLicenseFeatures } from '../../common/licensing'; import { BehaviorSubject, Observable, of } from 'rxjs'; +jest.useFakeTimers(); + const createConfig = (settings: Partial) => { return ConfigSchema.validate(settings); }; @@ -28,18 +35,20 @@ const http = httpServiceMock.createSetupContract(); const getCurrentUser = jest.fn().mockReturnValue({ username: 'jdoe', roles: ['admin'] }); const getSpaceId = jest.fn().mockReturnValue('default'); const getSID = jest.fn().mockResolvedValue('SESSION_ID'); +const recordAuditLoggingUsage = jest.fn(); beforeEach(() => { logger.info.mockClear(); logging.configure.mockClear(); + recordAuditLoggingUsage.mockClear(); http.registerOnPostAuth.mockClear(); }); describe('#setup', () => { it('returns the expected contract', () => { - const auditService = new AuditService(logger); + const audit = new AuditService(logger); expect( - auditService.setup({ + audit.setup({ license, config, logging, @@ -47,6 +56,7 @@ describe('#setup', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }) ).toMatchInlineSnapshot(` Object { @@ -54,10 +64,12 @@ describe('#setup', () => { "getLogger": [Function], } `); + audit.stop(); }); it('configures logging correctly when using ecs logger', async () => { - new AuditService(logger).setup({ + const audit = new AuditService(logger); + audit.setup({ license, config: { enabled: true, @@ -73,12 +85,67 @@ describe('#setup', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }); expect(logging.configure).toHaveBeenCalledWith(expect.any(Observable)); + audit.stop(); + }); + + it('records feature usage correctly when using ecs logger', async () => { + const audit = new AuditService(logger); + audit.setup({ + license: licenseMock.create({ + allowAuditLogging: true, + }), + config: { + enabled: true, + appender: { + kind: 'console', + layout: { + kind: 'pattern', + }, + }, + }, + logging, + http, + getCurrentUser, + getSpaceId, + getSID, + recordAuditLoggingUsage, + }); + expect(recordAuditLoggingUsage).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(RECORD_USAGE_INTERVAL); + expect(recordAuditLoggingUsage).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(RECORD_USAGE_INTERVAL); + expect(recordAuditLoggingUsage).toHaveBeenCalledTimes(3); + audit.stop(); + }); + + it('does not record feature usage when disabled', async () => { + const audit = new AuditService(logger); + audit.setup({ + license, + config: { + enabled: false, + }, + logging, + http, + getCurrentUser, + getSpaceId, + getSID, + recordAuditLoggingUsage, + }); + expect(recordAuditLoggingUsage).not.toHaveBeenCalled(); + jest.advanceTimersByTime(RECORD_USAGE_INTERVAL); + expect(recordAuditLoggingUsage).not.toHaveBeenCalled(); + jest.advanceTimersByTime(RECORD_USAGE_INTERVAL); + expect(recordAuditLoggingUsage).not.toHaveBeenCalled(); + audit.stop(); }); it('registers post auth hook', () => { - new AuditService(logger).setup({ + const audit = new AuditService(logger); + audit.setup({ license, config, logging, @@ -86,14 +153,17 @@ describe('#setup', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }); expect(http.registerOnPostAuth).toHaveBeenCalledWith(expect.any(Function)); + audit.stop(); }); }); describe('#asScoped', () => { it('logs event enriched with meta data', async () => { - const audit = new AuditService(logger).setup({ + const audit = new AuditService(logger); + const auditSetup = audit.setup({ license, config, logging, @@ -101,12 +171,13 @@ describe('#asScoped', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }); const request = httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, }); - await audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + await auditSetup.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); expect(logger.info).toHaveBeenCalledWith('MESSAGE', { ecs: { version: '1.6.0' }, event: { action: 'ACTION' }, @@ -115,10 +186,12 @@ describe('#asScoped', () => { trace: { id: 'REQUEST_ID' }, user: { name: 'jdoe', roles: ['admin'] }, }); + audit.stop(); }); it('does not log to audit logger if event matches ignore filter', async () => { - const audit = new AuditService(logger).setup({ + const audit = new AuditService(logger); + const auditSetup = audit.setup({ license, config: { enabled: true, @@ -129,17 +202,20 @@ describe('#asScoped', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }); const request = httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, }); - await audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + await auditSetup.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); expect(logger.info).not.toHaveBeenCalled(); + audit.stop(); }); it('does not log to audit logger if no event was generated', async () => { - const audit = new AuditService(logger).setup({ + const audit = new AuditService(logger); + const auditSetup = audit.setup({ license, config: { enabled: true, @@ -150,13 +226,15 @@ describe('#asScoped', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }); const request = httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, }); - await audit.asScoped(request).log(undefined); + await auditSetup.asScoped(request).log(undefined); expect(logger.info).not.toHaveBeenCalled(); + audit.stop(); }); }); @@ -376,6 +454,7 @@ describe('#getLogger', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }); const auditLogger = auditService.getLogger(pluginId); @@ -407,6 +486,7 @@ describe('#getLogger', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }); const auditLogger = auditService.getLogger(pluginId); @@ -446,6 +526,7 @@ describe('#getLogger', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }); const auditLogger = auditService.getLogger(pluginId); @@ -475,6 +556,7 @@ describe('#getLogger', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }); const auditLogger = auditService.getLogger(pluginId); @@ -505,6 +587,7 @@ describe('#getLogger', () => { getCurrentUser, getSpaceId, getSID, + recordAuditLoggingUsage, }); const auditLogger = auditService.getLogger(pluginId); diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index 4ad1f873581c9..7ecdef0ab4e4e 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -20,6 +20,7 @@ import { AuditEvent, httpRequestEvent } from './audit_events'; import { SecurityPluginSetup } from '..'; export const ECS_VERSION = '1.6.0'; +export const RECORD_USAGE_INTERVAL = 60 * 60 * 1000; // 1 hour /** * @deprecated @@ -58,6 +59,7 @@ interface AuditServiceSetupParams { getSpaceId( request: KibanaRequest ): ReturnType | undefined; + recordAuditLoggingUsage(): void; } export class AuditService { @@ -69,8 +71,8 @@ export class AuditService { * @deprecated */ private allowLegacyAuditLogging = false; - private ecsLogger: Logger; + private usageIntervalId?: NodeJS.Timeout; constructor(private readonly logger: Logger) { this.ecsLogger = logger.get('ecs'); @@ -84,6 +86,7 @@ export class AuditService { getCurrentUser, getSID, getSpaceId, + recordAuditLoggingUsage, }: AuditServiceSetupParams): AuditServiceSetup { if (config.enabled && !config.appender) { this.licenseFeaturesSubscription = license.features$.subscribe( @@ -101,6 +104,20 @@ export class AuditService { ) ); + // Record feature usage at a regular interval if enabled and license allows + if (config.enabled && config.appender) { + license.features$.subscribe((features) => { + clearInterval(this.usageIntervalId!); + if (features.allowAuditLogging) { + recordAuditLoggingUsage(); + this.usageIntervalId = setInterval(recordAuditLoggingUsage, RECORD_USAGE_INTERVAL); + if (this.usageIntervalId.unref) { + this.usageIntervalId.unref(); + } + } + }); + } + /** * Creates an {@link AuditLogger} scoped to the current request. * @@ -198,6 +215,7 @@ export class AuditService { this.licenseFeaturesSubscription.unsubscribe(); this.licenseFeaturesSubscription = undefined; } + clearInterval(this.usageIntervalId!); } } 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({ ))} - - - - - - -