diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index accc99170bb70..df3a56dd35130 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -29,33 +29,32 @@ /src/plugins/dashboard/ @elastic/kibana-app # App Architecture +/examples/url_generators_examples/ @elastic/kibana-app-arch +/examples/url_generators_explorer/ @elastic/kibana-app-arch /packages/kbn-interpreter/ @elastic/kibana-app-arch -/src/legacy/core_plugins/data/ @elastic/kibana-app-arch -/src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @elastic/kibana-app-arch /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch /src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana/public/management/ @elastic/kibana-app-arch -/src/legacy/core_plugins/kibana/server/field_formats/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana/server/routes/api/management/ @elastic/kibana-app-arch -/src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch +/src/plugins/advanced_settings/ @elastic/kibana-app-arch /src/plugins/bfetch/ @elastic/kibana-app-arch /src/plugins/data/ @elastic/kibana-app-arch /src/plugins/embeddable/ @elastic/kibana-app-arch /src/plugins/expressions/ @elastic/kibana-app-arch /src/plugins/inspector/ @elastic/kibana-app-arch /src/plugins/kibana_react/ @elastic/kibana-app-arch +/src/plugins/kibana_react/public/code_editor @elastic/kibana-canvas /src/plugins/kibana_utils/ @elastic/kibana-app-arch /src/plugins/management/ @elastic/kibana-app-arch /src/plugins/navigation/ @elastic/kibana-app-arch +/src/plugins/share/ @elastic/kibana-app-arch /src/plugins/ui_actions/ @elastic/kibana-app-arch /src/plugins/visualizations/ @elastic/kibana-app-arch -/src/plugins/share/ @elastic/kibana-app-arch -/examples/url_generators_examples/ @elastic/kibana-app-arch -/examples/url_generators_explorer/ @elastic/kibana-app-arch /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch +/x-pack/plugins/data_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch # APM @@ -75,9 +74,9 @@ # Observability UIs /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/infra/ @elastic/logs-metrics-ui -/x-pack/plugins/ingest_manager/ @elastic/ingest -/x-pack/legacy/plugins/ingest_manager/ @elastic/ingest -/x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest +/x-pack/plugins/ingest_manager/ @elastic/ingest-management +/x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management +/x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui # Machine Learning diff --git a/docs/developer/core/development-functional-tests.asciidoc b/docs/developer/core/development-functional-tests.asciidoc index dcb3d65b8b83f..51b5273851ce7 100644 --- a/docs/developer/core/development-functional-tests.asciidoc +++ b/docs/developer/core/development-functional-tests.asciidoc @@ -178,6 +178,25 @@ To run tests on Firefox locally, use `config.firefox.js`: node scripts/functional_test_runner --config test/functional/config.firefox.js ----------- +[float] +===== Using the test_user service + +Tests should run at the positive security boundry condition, meaning that they should be run with the mimimum privileges required (and documented) and not as the superuser. + This prevents the type of regression where additional privleges accidentally become required to perform the same action. + +The functional UI tests now default to logging in with a user named `test_user` and the roles of this user can be changed dynamically without logging in and out. + +In order to achieve this a new service was introduced called `createTestUserService` (see `test/common/services/security/test_user.ts`). The purpose of this test user service is to create roles defined in the test config files and setRoles() or restoreDefaults(). + +An example of how to set the role like how its defined below: + +`await security.testUser.setRoles(['kibana_user', 'kibana_date_nanos']);` + +Here we are setting the `test_user` to have the `kibana_user` role and also role access to a specific data index (`kibana_date_nanos`). + +Tests should normally setRoles() in the before() and restoreDefaults() in the after(). + + [float] ===== Anatomy of a test file diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getiscollapsed_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md similarity index 51% rename from docs/development/core/public/kibana-plugin-core-public.chromestart.getiscollapsed_.md rename to docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md index 205f863526e22..78a4442a651e6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getiscollapsed_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md @@ -1,15 +1,15 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getIsCollapsed$](./kibana-plugin-core-public.chromestart.getiscollapsed_.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getIsNavDrawerLocked$](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) -## ChromeStart.getIsCollapsed$() method +## ChromeStart.getIsNavDrawerLocked$() method -Get an observable of the current collapsed state of the chrome. +Get an observable of the current locked state of the nav drawer. Signature: ```typescript -getIsCollapsed$(): Observable; +getIsNavDrawerLocked$(): Observable; ``` Returns: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index 7d9d47df544d0..c179e089d7cfd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -56,7 +56,7 @@ core.chrome.setHelpExtension(elem => { | [getBrand$()](./kibana-plugin-core-public.chromestart.getbrand_.md) | Get an observable of the current brand information. | | [getBreadcrumbs$()](./kibana-plugin-core-public.chromestart.getbreadcrumbs_.md) | Get an observable of the current list of breadcrumbs | | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | -| [getIsCollapsed$()](./kibana-plugin-core-public.chromestart.getiscollapsed_.md) | Get an observable of the current collapsed state of the chrome. | +| [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | | [getIsVisible$()](./kibana-plugin-core-public.chromestart.getisvisible_.md) | Get an observable of the current visibility state of the chrome. | | [removeApplicationClass(className)](./kibana-plugin-core-public.chromestart.removeapplicationclass.md) | Remove a className added with addApplicationClass(). If className is unknown it is ignored. | | [setAppTitle(appTitle)](./kibana-plugin-core-public.chromestart.setapptitle.md) | Sets the current app's title | @@ -65,6 +65,5 @@ core.chrome.setHelpExtension(elem => { | [setBreadcrumbs(newBreadcrumbs)](./kibana-plugin-core-public.chromestart.setbreadcrumbs.md) | Override the current set of breadcrumbs | | [setHelpExtension(helpExtension)](./kibana-plugin-core-public.chromestart.sethelpextension.md) | Override the current set of custom help content | | [setHelpSupportUrl(url)](./kibana-plugin-core-public.chromestart.sethelpsupporturl.md) | Override the default support URL shown in the help menu | -| [setIsCollapsed(isCollapsed)](./kibana-plugin-core-public.chromestart.setiscollapsed.md) | Set the collapsed state of the chrome navigation. | | [setIsVisible(isVisible)](./kibana-plugin-core-public.chromestart.setisvisible.md) | Set the temporary visibility for the chrome. This does nothing if the chrome is hidden by default and should be used to hide the chrome for things like full-screen modes with an exit button. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setiscollapsed.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setiscollapsed.md deleted file mode 100644 index b1843ef326d96..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.setiscollapsed.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [setIsCollapsed](./kibana-plugin-core-public.chromestart.setiscollapsed.md) - -## ChromeStart.setIsCollapsed() method - -Set the collapsed state of the chrome navigation. - -Signature: - -```typescript -setIsCollapsed(isCollapsed: boolean): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| isCollapsed | boolean | | - -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md b/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md index 204d8a786fede..3bbabc04f2500 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md @@ -14,7 +14,7 @@ validate: RouteValidatorFullConfig | false; ## Remarks -You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { allowUnknowns: true })`; +You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { unknowns: 'allow' })`; ## Example @@ -49,7 +49,7 @@ router.get({ path: 'path/{id}', validate: { // handler has access to raw non-validated params in runtime - params: schema.object({}, { allowUnknowns: true }) + params: schema.object({}, { unknowns: 'allow' }) }, }, (context, req, res,) { diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index e03072f9a41c3..7fd65e5db35f3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -44,8 +44,8 @@ esFilters: { getPhraseFilterField: (filter: import("../common").PhraseFilter) => string; getPhraseFilterValue: (filter: import("../common").PhraseFilter) => string | number | boolean; getDisplayValueFromFilter: typeof getDisplayValueFromFilter; - compareFilters: (first: import("../common").Filter | import("../common").Filter[], second: import("../common").Filter | import("../common").Filter[], comparatorOptions?: import("./query/filter_manager/lib/compare_filters").FilterCompareOptions) => boolean; - COMPARE_ALL_OPTIONS: import("./query/filter_manager/lib/compare_filters").FilterCompareOptions; + compareFilters: (first: import("../common").Filter | import("../common").Filter[], second: import("../common").Filter | import("../common").Filter[], comparatorOptions?: import("../common").FilterCompareOptions) => boolean; + COMPARE_ALL_OPTIONS: import("../common").FilterCompareOptions; generateFilters: typeof generateFilters; onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.icancel.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.icancel.md deleted file mode 100644 index 27141c68ae1a7..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.icancel.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ICancel](./kibana-plugin-plugins-data-server.icancel.md) - -## ICancel type - -Signature: - -```typescript -export declare type ICancel = (id: string) => Promise; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md new file mode 100644 index 0000000000000..99c30515e8da6 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchCancel](./kibana-plugin-plugins-data-server.isearchcancel.md) + +## ISearchCancel type + +Signature: + +```typescript +export declare type ISearchCancel = (id: string) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 12d53f1a35ea0..e756eb9b72905 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -67,9 +67,9 @@ | Type Alias | Description | | --- | --- | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-server.fieldformatsgetconfigfn.md) | | -| [ICancel](./kibana-plugin-plugins-data-server.icancel.md) | | | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-server.ifieldformatsregistry.md) | | | [ISearch](./kibana-plugin-plugins-data-server.isearch.md) | | +| [ISearchCancel](./kibana-plugin-plugins-data-server.isearchcancel.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | | [TSearchStrategyProvider](./kibana-plugin-plugins-data-server.tsearchstrategyprovider.md) | Search strategy provider creates an instance of a search strategy with the request handler context bound to it. This way every search strategy can use whatever information they require from the request context. | diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 8586d26e9a07a..6645f49029a51 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -20,9 +20,7 @@ which support the same values as <>. To control how data is collected from your {es} nodes, you configure {ref}/monitoring-settings.html[`xpack.monitoring.collection` settings] in `elasticsearch.yml`. To control how monitoring data is collected -from Logstash, you configure -{logstash-ref}/monitoring-internal-collection.html#monitoring-settings[`xpack.monitoring` settings] -in `logstash.yml`. +from Logstash, configure monitoring settings in `logstash.yml`. For more information, see {ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. diff --git a/package.json b/package.json index b3dcfb2aa3b0a..aa9c8f6c40160 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "url": "https://github.com/elastic/kibana.git" }, "resolutions": { - "**/@types/node": "10.12.27", + "**/@types/node": ">=10.17.17 <10.20.0", "**/@types/react": "^16.9.19", "**/@types/react-router": "^5.1.3", "**/@types/hapi": "^17.0.18", @@ -119,7 +119,7 @@ "@elastic/apm-rum": "^4.6.0", "@elastic/charts": "^17.1.1", "@elastic/datemath": "5.0.2", - "@elastic/ems-client": "7.6.0", + "@elastic/ems-client": "7.7.0", "@elastic/eui": "20.0.2", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", @@ -239,7 +239,7 @@ "react-resize-detector": "^4.2.0", "react-router-dom": "^5.1.2", "react-sizeme": "^2.3.6", - "react-use": "^13.13.0", + "react-use": "^13.27.0", "reactcss": "1.2.3", "redux": "^4.0.5", "redux-actions": "^2.6.5", @@ -314,6 +314,7 @@ "@types/cheerio": "^0.22.10", "@types/chromedriver": "^2.38.0", "@types/classnames": "^2.2.9", + "@types/color": "^3.0.0", "@types/d3": "^3.5.43", "@types/dedent": "^0.7.0", "@types/deep-freeze-strict": "^1.1.0", @@ -349,7 +350,7 @@ "@types/mocha": "^5.2.7", "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", - "@types/node": "^10.12.27", + "@types/node": ">=10.17.17 <10.20.0", "@types/node-forge": "^0.9.0", "@types/normalize-path": "^3.0.0", "@types/numeral": "^0.0.26", diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md index 8719a2ae558ab..a4f2c1f6458cf 100644 --- a/packages/kbn-config-schema/README.md +++ b/packages/kbn-config-schema/README.md @@ -239,7 +239,7 @@ __Output type:__ `{ [K in keyof TProps]: TypeOf } as TObject` __Options:__ * `defaultValue: TObject | Reference | (() => TObject)` - defines a default value, see [Default values](#default-values) section for more details. * `validate: (value: TObject) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. - * `allowUnknowns: boolean` - indicates whether unknown object properties should be allowed. It's `false` by default. + * `unknowns: 'allow' | 'ignore' | 'forbid'` - indicates whether unknown object properties should be allowed, ignored, or forbidden. It's `forbid` by default. __Usage:__ ```typescript @@ -250,7 +250,7 @@ const valueSchema = schema.object({ ``` __Notes:__ -* Using `allowUnknowns` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. +* Using `unknowns: 'allow'` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. * Currently `schema.object()` always has a default value of `{}`, but this may change in the near future. Try to not rely on this behaviour and specify default value explicitly or use `schema.maybe()` if the value is optional. * `schema.object()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is a plain object. diff --git a/packages/kbn-config-schema/src/internals/index.ts b/packages/kbn-config-schema/src/internals/index.ts index 8f5d09e5b8b49..f84e14d2f741d 100644 --- a/packages/kbn-config-schema/src/internals/index.ts +++ b/packages/kbn-config-schema/src/internals/index.ts @@ -314,7 +314,8 @@ export const internals = Joi.extend([ for (const [entryKey, entryValue] of value) { const { value: validatedEntryKey, error: keyError } = Joi.validate( entryKey, - params.key + params.key, + { presence: 'required' } ); if (keyError) { @@ -323,7 +324,8 @@ export const internals = Joi.extend([ const { value: validatedEntryValue, error: valueError } = Joi.validate( entryValue, - params.value + params.value, + { presence: 'required' } ); if (valueError) { @@ -374,7 +376,8 @@ export const internals = Joi.extend([ for (const [entryKey, entryValue] of Object.entries(value)) { const { value: validatedEntryKey, error: keyError } = Joi.validate( entryKey, - params.key + params.key, + { presence: 'required' } ); if (keyError) { @@ -383,7 +386,8 @@ export const internals = Joi.extend([ const { value: validatedEntryValue, error: valueError } = Joi.validate( entryValue, - params.value + params.value, + { presence: 'required' } ); if (valueError) { diff --git a/packages/kbn-config-schema/src/types/map_of_type.test.ts b/packages/kbn-config-schema/src/types/map_of_type.test.ts index b015f51bdc8ad..1c5a227ef0fac 100644 --- a/packages/kbn-config-schema/src/types/map_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/map_of_type.test.ts @@ -159,6 +159,24 @@ test('object within mapOf', () => { expect(type.validate(value)).toEqual(expected); }); +test('enforces required object fields within mapOf', () => { + const type = schema.mapOf( + schema.string(), + schema.object({ + bar: schema.object({ + baz: schema.number(), + }), + }) + ); + const value = { + foo: {}, + }; + + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[foo.bar.baz]: expected value of type [number] but got [undefined]"` + ); +}); + test('error preserves full path', () => { const type = schema.object({ grandParentKey: schema.object({ diff --git a/packages/kbn-config-schema/src/types/map_type.ts b/packages/kbn-config-schema/src/types/map_type.ts index 231c3726ae9d5..6da664bf95616 100644 --- a/packages/kbn-config-schema/src/types/map_type.ts +++ b/packages/kbn-config-schema/src/types/map_type.ts @@ -57,7 +57,10 @@ export class MapOfType extends Type> { path.length, 0, // If `key` validation failed, let's stress that to make error more obvious. - type === 'map.key' ? `key("${entryKey}")` : entryKey.toString() + type === 'map.key' ? `key("${entryKey}")` : entryKey.toString(), + // Error could have happened deep inside value/key schema and error message should + // include full path. + ...(reason instanceof SchemaTypeError ? reason.path : []) ); return reason instanceof SchemaTypesError diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 29e341983fde9..47a0f5f7a5491 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -276,10 +276,10 @@ test('individual keys can validated', () => { ); }); -test('allow unknown keys when allowUnknowns = true', () => { +test('allow unknown keys when unknowns = `allow`', () => { const type = schema.object( { foo: schema.string({ defaultValue: 'test' }) }, - { allowUnknowns: true } + { unknowns: 'allow' } ); expect( @@ -292,10 +292,10 @@ test('allow unknown keys when allowUnknowns = true', () => { }); }); -test('allowUnknowns = true affects only own keys', () => { +test('unknowns = `allow` affects only own keys', () => { const type = schema.object( { foo: schema.object({ bar: schema.string() }) }, - { allowUnknowns: true } + { unknowns: 'allow' } ); expect(() => @@ -308,10 +308,10 @@ test('allowUnknowns = true affects only own keys', () => { ).toThrowErrorMatchingInlineSnapshot(`"[foo.baz]: definition for this key is missing"`); }); -test('does not allow unknown keys when allowUnknowns = false', () => { +test('does not allow unknown keys when unknowns = `forbid`', () => { const type = schema.object( { foo: schema.string({ defaultValue: 'test' }) }, - { allowUnknowns: false } + { unknowns: 'forbid' } ); expect(() => type.validate({ @@ -319,3 +319,34 @@ test('does not allow unknown keys when allowUnknowns = false', () => { }) ).toThrowErrorMatchingInlineSnapshot(`"[bar]: definition for this key is missing"`); }); + +test('allow and remove unknown keys when unknowns = `ignore`', () => { + const type = schema.object( + { foo: schema.string({ defaultValue: 'test' }) }, + { unknowns: 'ignore' } + ); + + expect( + type.validate({ + bar: 'baz', + }) + ).toEqual({ + foo: 'test', + }); +}); + +test('unknowns = `ignore` affects only own keys', () => { + const type = schema.object( + { foo: schema.object({ bar: schema.string() }) }, + { unknowns: 'ignore' } + ); + + expect(() => + type.validate({ + foo: { + bar: 'bar', + baz: 'baz', + }, + }) + ).toThrowErrorMatchingInlineSnapshot(`"[foo.baz]: definition for this key is missing"`); +}); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index f34acd0d2ce65..5a50e714a5931 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -30,17 +30,25 @@ export type TypeOf> = RT['type']; // this might not have perfect _rendering_ output, but it will be typed. export type ObjectResultType

= Readonly<{ [K in keyof P]: TypeOf }>; +interface UnknownOptions { + /** + * Options for dealing with unknown keys: + * - allow: unknown keys will be permitted + * - ignore: unknown keys will not fail validation, but will be stripped out + * - forbid (default): unknown keys will fail validation + */ + unknowns?: 'allow' | 'ignore' | 'forbid'; +} + export type ObjectTypeOptions

= TypeOptions< { [K in keyof P]: TypeOf } -> & { - /** Should uknown keys not be defined in the schema be allowed. Defaults to `false` */ - allowUnknowns?: boolean; -}; +> & + UnknownOptions; export class ObjectType

extends Type> { private props: Record; - constructor(props: P, { allowUnknowns = false, ...typeOptions }: ObjectTypeOptions

= {}) { + constructor(props: P, { unknowns = 'forbid', ...typeOptions }: ObjectTypeOptions

= {}) { const schemaKeys = {} as Record; for (const [key, value] of Object.entries(props)) { schemaKeys[key] = value.getSchema(); @@ -50,7 +58,8 @@ export class ObjectType

extends Type> .keys(schemaKeys) .default() .optional() - .unknown(Boolean(allowUnknowns)); + .unknown(unknowns === 'allow') + .options({ stripUnknown: { objects: unknowns === 'ignore' } }); super(schema, typeOptions); this.props = schemaKeys; diff --git a/packages/kbn-config-schema/src/types/record_of_type.test.ts b/packages/kbn-config-schema/src/types/record_of_type.test.ts index ef15e7b0f6ad6..aee7dde71c3e4 100644 --- a/packages/kbn-config-schema/src/types/record_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/record_of_type.test.ts @@ -159,6 +159,24 @@ test('object within recordOf', () => { expect(type.validate(value)).toEqual({ foo: { bar: 123 } }); }); +test('enforces required object fields within recordOf', () => { + const type = schema.recordOf( + schema.string(), + schema.object({ + bar: schema.object({ + baz: schema.number(), + }), + }) + ); + const value = { + foo: {}, + }; + + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[foo.bar.baz]: expected value of type [number] but got [undefined]"` + ); +}); + test('error preserves full path', () => { const type = schema.object({ grandParentKey: schema.object({ diff --git a/packages/kbn-config-schema/src/types/record_type.ts b/packages/kbn-config-schema/src/types/record_type.ts index c6d4b4d71b4f1..ef9e70cbabc08 100644 --- a/packages/kbn-config-schema/src/types/record_type.ts +++ b/packages/kbn-config-schema/src/types/record_type.ts @@ -49,7 +49,10 @@ export class RecordOfType extends Type> { path.length, 0, // If `key` validation failed, let's stress that to make error more obvious. - type === 'record.key' ? `key("${entryKey}")` : entryKey.toString() + type === 'record.key' ? `key("${entryKey}")` : entryKey.toString(), + // Error could have happened deep inside value/key schema and error message should + // include full path. + ...(reason instanceof SchemaTypeError ? reason.path : []) ); return reason instanceof SchemaTypesError diff --git a/packages/kbn-dev-utils/src/run/run.ts b/packages/kbn-dev-utils/src/run/run.ts index e185f86cc3bf7..35477e988d837 100644 --- a/packages/kbn-dev-utils/src/run/run.ts +++ b/packages/kbn-dev-utils/src/run/run.ts @@ -17,6 +17,8 @@ * under the License. */ +import { inspect } from 'util'; + // @ts-ignore @types are outdated and module is super simple import exitHook from 'exit-hook'; @@ -62,7 +64,11 @@ export async function run(fn: RunFn, options: Options = {}) { process.on('unhandledRejection', error => { log.error('UNHANDLED PROMISE REJECTION'); - log.error(error); + log.error( + error instanceof Error + ? error + : new Error(`non-Error type rejection value: ${inspect(error)}`) + ); process.exit(1); }); diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 338d489300526..9fab74ea47a87 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(704); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(705); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); @@ -39806,6 +39806,7 @@ exports.isFailError = fail_1.isFailError; */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); +const util_1 = __webpack_require__(29); // @ts-ignore @types are outdated and module is super simple const exit_hook_1 = tslib_1.__importDefault(__webpack_require__(348)); const tooling_log_1 = __webpack_require__(415); @@ -39825,7 +39826,9 @@ async function run(fn, options = {}) { }); process.on('unhandledRejection', error => { log.error('UNHANDLED PROMISE REJECTION'); - log.error(error); + log.error(error instanceof Error + ? error + : new Error(`non-Error type rejection value: ${util_1.inspect(error)}`)); process.exit(1); }); const handleErrorWithoutExit = (error) => { @@ -57074,7 +57077,7 @@ async function getChangesForProjects(projects, kbn, log) { log.verbose('getting changed files'); const { stdout - } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['ls-files', '-dmt', '--', ...Array.from(projects.values()).map(p => p.path)], { + } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['ls-files', '-dmt', '--', ...Array.from(projects.values()).filter(p => kbn.isPartOfRepo(p)).map(p => p.path)], { cwd: kbn.getAbsolute() }); const output = stdout.trim(); @@ -57117,6 +57120,11 @@ async function getChangesForProjects(projects, kbn, log) { const changesByProject = new Map(); for (const project of sortedRelevantProjects) { + if (kbn.isOutsideRepo(project)) { + changesByProject.set(project, undefined); + continue; + } + const ownChanges = new Map(); const prefix = kbn.getRelative(project.path); @@ -57141,6 +57149,10 @@ async function getChangesForProjects(projects, kbn, log) { async function getLatestSha(project, kbn) { + if (kbn.isOutsideRepo(project)) { + return; + } + const { stdout } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['log', '-n', '1', '--pretty=format:%H', '--', project.path], { @@ -57200,7 +57212,7 @@ async function getChecksum(project, changes, yarnLock, kbn, log) { log.verbose(`[${project.name}] local sha:`, sha); } - if (Array.from(changes.values()).includes('invalid')) { + if (!changes || Array.from(changes.values()).includes('invalid')) { log.warning(`[${project.name}] unable to determine local changes, caching disabled`); return; } @@ -79162,8 +79174,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(700); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(501); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(579); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(704); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(501); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(579); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -79192,6 +79206,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope + /** * Helper class for dealing with a set of projects as children of * the Kibana project. The kbn/pm is currently implemented to be @@ -79206,7 +79221,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope class Kibana { static async loadFrom(rootPath) { - return new Kibana((await Object(_projects__WEBPACK_IMPORTED_MODULE_2__["getProjects"])(rootPath, Object(_config__WEBPACK_IMPORTED_MODULE_3__["getProjectPaths"])({ + return new Kibana((await Object(_projects__WEBPACK_IMPORTED_MODULE_3__["getProjects"])(rootPath, Object(_config__WEBPACK_IMPORTED_MODULE_4__["getProjectPaths"])({ rootPath })))); } @@ -79265,7 +79280,7 @@ class Kibana { getProjectAndDeps(name) { const project = this.getProject(name); - return Object(_projects__WEBPACK_IMPORTED_MODULE_2__["includeTransitiveProjects"])([project], this.allWorkspaceProjects); + return Object(_projects__WEBPACK_IMPORTED_MODULE_3__["includeTransitiveProjects"])([project], this.allWorkspaceProjects); } /** filter the projects to just those matching certain paths/include/exclude tags */ @@ -79274,7 +79289,7 @@ class Kibana { const allProjects = this.getAllProjects(); const filteredProjects = new Map(); const pkgJsonPaths = Array.from(allProjects.values()).map(p => p.packageJsonLocation); - const filteredPkgJsonGlobs = Object(_config__WEBPACK_IMPORTED_MODULE_3__["getProjectPaths"])(_objectSpread({}, options, { + const filteredPkgJsonGlobs = Object(_config__WEBPACK_IMPORTED_MODULE_4__["getProjectPaths"])(_objectSpread({}, options, { rootPath: this.kibanaProject.path })).map(g => path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(g, 'package.json')); const matchingPkgJsonPaths = multimatch__WEBPACK_IMPORTED_MODULE_1___default()(pkgJsonPaths, filteredPkgJsonGlobs); @@ -79292,6 +79307,14 @@ class Kibana { return filteredProjects; } + isPartOfRepo(project) { + return project.path === this.kibanaProject.path || is_path_inside__WEBPACK_IMPORTED_MODULE_2___default()(project.path, this.kibanaProject.path); + } + + isOutsideRepo(project) { + return !this.isPartOfRepo(project); + } + } /***/ }), @@ -79385,14 +79408,42 @@ module.exports = arrify; /***/ }), /* 704 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const path = __webpack_require__(16); + +module.exports = (childPath, parentPath) => { + childPath = path.resolve(childPath); + parentPath = path.resolve(parentPath); + + if (process.platform === 'win32') { + childPath = childPath.toLowerCase(); + parentPath = parentPath.toLowerCase(); + } + + if (childPath === parentPath) { + return false; + } + + childPath += path.sep; + parentPath += path.sep; + + return childPath.startsWith(parentPath); +}; + + +/***/ }), +/* 705 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(705); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(928); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(929); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79417,13 +79468,13 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 705 */ +/* 706 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(707); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(587); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); @@ -79565,7 +79616,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 706 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79573,13 +79624,13 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(379); const path = __webpack_require__(16); const os = __webpack_require__(11); -const pAll = __webpack_require__(707); -const arrify = __webpack_require__(709); -const globby = __webpack_require__(710); +const pAll = __webpack_require__(708); +const arrify = __webpack_require__(710); +const globby = __webpack_require__(711); const isGlob = __webpack_require__(605); -const cpFile = __webpack_require__(913); -const junk = __webpack_require__(925); -const CpyError = __webpack_require__(926); +const cpFile = __webpack_require__(914); +const junk = __webpack_require__(926); +const CpyError = __webpack_require__(927); const defaultOptions = { ignoreJunk: true @@ -79698,12 +79749,12 @@ module.exports = (source, destination, { /***/ }), -/* 707 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(708); +const pMap = __webpack_require__(709); module.exports = (iterable, options) => pMap(iterable, element => element(), options); // TODO: Remove this for the next major release @@ -79711,7 +79762,7 @@ module.exports.default = module.exports; /***/ }), -/* 708 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79790,7 +79841,7 @@ module.exports.default = pMap; /***/ }), -/* 709 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79820,17 +79871,17 @@ module.exports = arrify; /***/ }), -/* 710 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(711); -const glob = __webpack_require__(713); -const fastGlob = __webpack_require__(718); -const dirGlob = __webpack_require__(906); -const gitignore = __webpack_require__(909); +const arrayUnion = __webpack_require__(712); +const glob = __webpack_require__(714); +const fastGlob = __webpack_require__(719); +const dirGlob = __webpack_require__(907); +const gitignore = __webpack_require__(910); const DEFAULT_FILTER = () => false; @@ -79975,12 +80026,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 711 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(712); +var arrayUniq = __webpack_require__(713); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -79988,7 +80039,7 @@ module.exports = function () { /***/ }), -/* 712 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80057,7 +80108,7 @@ if ('Set' in global) { /***/ }), -/* 713 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -80106,13 +80157,13 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(503) var minimatch = __webpack_require__(505) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(714) +var inherits = __webpack_require__(715) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(511) -var globSync = __webpack_require__(716) -var common = __webpack_require__(717) +var globSync = __webpack_require__(717) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -80853,7 +80904,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 714 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -80863,12 +80914,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(715); + module.exports = __webpack_require__(716); } /***/ }), -/* 715 */ +/* 716 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -80901,7 +80952,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 716 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -80911,12 +80962,12 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(503) var minimatch = __webpack_require__(505) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(713).Glob +var Glob = __webpack_require__(714).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(511) -var common = __webpack_require__(717) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -81393,7 +81444,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 717 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -81639,10 +81690,10 @@ function childrenIgnored (self, path) { /***/ }), -/* 718 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(719); +const pkg = __webpack_require__(720); module.exports = pkg.async; module.exports.default = pkg.async; @@ -81655,19 +81706,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 719 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(720); -var taskManager = __webpack_require__(721); -var reader_async_1 = __webpack_require__(877); -var reader_stream_1 = __webpack_require__(901); -var reader_sync_1 = __webpack_require__(902); -var arrayUtils = __webpack_require__(904); -var streamUtils = __webpack_require__(905); +var optionsManager = __webpack_require__(721); +var taskManager = __webpack_require__(722); +var reader_async_1 = __webpack_require__(878); +var reader_stream_1 = __webpack_require__(902); +var reader_sync_1 = __webpack_require__(903); +var arrayUtils = __webpack_require__(905); +var streamUtils = __webpack_require__(906); /** * Synchronous API. */ @@ -81733,7 +81784,7 @@ function isString(source) { /***/ }), -/* 720 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81771,13 +81822,13 @@ exports.prepare = prepare; /***/ }), -/* 721 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(722); +var patternUtils = __webpack_require__(723); /** * Generate tasks based on parent directory of each pattern. */ @@ -81868,16 +81919,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 722 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var globParent = __webpack_require__(723); -var isGlob = __webpack_require__(726); -var micromatch = __webpack_require__(727); +var globParent = __webpack_require__(724); +var isGlob = __webpack_require__(727); +var micromatch = __webpack_require__(728); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -82023,15 +82074,15 @@ exports.matchAny = matchAny; /***/ }), -/* 723 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(16); -var isglob = __webpack_require__(724); -var pathDirname = __webpack_require__(725); +var isglob = __webpack_require__(725); +var pathDirname = __webpack_require__(726); var isWin32 = __webpack_require__(11).platform() === 'win32'; module.exports = function globParent(str) { @@ -82054,7 +82105,7 @@ module.exports = function globParent(str) { /***/ }), -/* 724 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82085,7 +82136,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 725 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82235,7 +82286,7 @@ module.exports.win32 = win32; /***/ }), -/* 726 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82287,7 +82338,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 727 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82298,18 +82349,18 @@ module.exports = function isGlob(str, options) { */ var util = __webpack_require__(29); -var braces = __webpack_require__(728); -var toRegex = __webpack_require__(830); -var extend = __webpack_require__(838); +var braces = __webpack_require__(729); +var toRegex = __webpack_require__(831); +var extend = __webpack_require__(839); /** * Local dependencies */ -var compilers = __webpack_require__(841); -var parsers = __webpack_require__(873); -var cache = __webpack_require__(874); -var utils = __webpack_require__(875); +var compilers = __webpack_require__(842); +var parsers = __webpack_require__(874); +var cache = __webpack_require__(875); +var utils = __webpack_require__(876); var MAX_LENGTH = 1024 * 64; /** @@ -83171,7 +83222,7 @@ module.exports = micromatch; /***/ }), -/* 728 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83181,18 +83232,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(729); -var unique = __webpack_require__(741); -var extend = __webpack_require__(738); +var toRegex = __webpack_require__(730); +var unique = __webpack_require__(742); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(742); -var parsers = __webpack_require__(757); -var Braces = __webpack_require__(767); -var utils = __webpack_require__(743); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(758); +var Braces = __webpack_require__(768); +var utils = __webpack_require__(744); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -83496,15 +83547,15 @@ module.exports = braces; /***/ }), -/* 729 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(730); -var extend = __webpack_require__(738); -var not = __webpack_require__(740); +var define = __webpack_require__(731); +var extend = __webpack_require__(739); +var not = __webpack_require__(741); var MAX_LENGTH = 1024 * 64; /** @@ -83651,7 +83702,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 730 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83664,7 +83715,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(731); +var isDescriptor = __webpack_require__(732); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -83689,7 +83740,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 731 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83702,9 +83753,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(732); -var isAccessor = __webpack_require__(733); -var isData = __webpack_require__(736); +var typeOf = __webpack_require__(733); +var isAccessor = __webpack_require__(734); +var isData = __webpack_require__(737); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -83718,7 +83769,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 732 */ +/* 733 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -83871,7 +83922,7 @@ function isBuffer(val) { /***/ }), -/* 733 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83884,7 +83935,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(734); +var typeOf = __webpack_require__(735); // accessor descriptor properties var accessor = { @@ -83947,10 +83998,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 734 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -84069,7 +84120,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 735 */ +/* 736 */ /***/ (function(module, exports) { /*! @@ -84096,7 +84147,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 736 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84109,7 +84160,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(737); +var typeOf = __webpack_require__(738); // data descriptor properties var data = { @@ -84158,10 +84209,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 737 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -84280,13 +84331,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 738 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(739); +var isObject = __webpack_require__(740); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -84320,7 +84371,7 @@ function hasOwn(obj, key) { /***/ }), -/* 739 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84340,13 +84391,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 740 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(738); +var extend = __webpack_require__(739); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -84413,7 +84464,7 @@ module.exports = toRegex; /***/ }), -/* 741 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84463,13 +84514,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 742 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(743); +var utils = __webpack_require__(744); module.exports = function(braces, options) { braces.compiler @@ -84752,25 +84803,25 @@ function hasQueue(node) { /***/ }), -/* 743 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(744); +var splitString = __webpack_require__(745); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(738); -utils.flatten = __webpack_require__(750); -utils.isObject = __webpack_require__(748); -utils.fillRange = __webpack_require__(751); -utils.repeat = __webpack_require__(756); -utils.unique = __webpack_require__(741); +utils.extend = __webpack_require__(739); +utils.flatten = __webpack_require__(751); +utils.isObject = __webpack_require__(749); +utils.fillRange = __webpack_require__(752); +utils.repeat = __webpack_require__(757); +utils.unique = __webpack_require__(742); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -85102,7 +85153,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 744 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85115,7 +85166,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(745); +var extend = __webpack_require__(746); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -85280,14 +85331,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 745 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(746); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(747); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -85347,7 +85398,7 @@ function isEnum(obj, key) { /***/ }), -/* 746 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85360,7 +85411,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -85368,7 +85419,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 747 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85381,7 +85432,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(749); function isObjectObject(o) { return isObject(o) === true @@ -85412,7 +85463,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 748 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85431,7 +85482,7 @@ module.exports = function isObject(val) { /***/ }), -/* 749 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85478,7 +85529,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 750 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85507,7 +85558,7 @@ function flat(arr, res) { /***/ }), -/* 751 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85521,10 +85572,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(752); -var extend = __webpack_require__(738); -var repeat = __webpack_require__(754); -var toRegex = __webpack_require__(755); +var isNumber = __webpack_require__(753); +var extend = __webpack_require__(739); +var repeat = __webpack_require__(755); +var toRegex = __webpack_require__(756); /** * Return a range of numbers or letters. @@ -85722,7 +85773,7 @@ module.exports = fillRange; /***/ }), -/* 752 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85735,7 +85786,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(754); module.exports = function isNumber(num) { var type = typeOf(num); @@ -85751,10 +85802,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 753 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -85873,7 +85924,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 754 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85950,7 +86001,7 @@ function repeat(str, num) { /***/ }), -/* 755 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85963,8 +86014,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(754); -var isNumber = __webpack_require__(752); +var repeat = __webpack_require__(755); +var isNumber = __webpack_require__(753); var cache = {}; function toRegexRange(min, max, options) { @@ -86251,7 +86302,7 @@ module.exports = toRegexRange; /***/ }), -/* 756 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86276,14 +86327,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 757 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(758); -var utils = __webpack_require__(743); +var Node = __webpack_require__(759); +var utils = __webpack_require__(744); /** * Braces parsers @@ -86643,15 +86694,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 758 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(748); -var define = __webpack_require__(759); -var utils = __webpack_require__(766); +var isObject = __webpack_require__(749); +var define = __webpack_require__(760); +var utils = __webpack_require__(767); var ownNames; /** @@ -87142,7 +87193,7 @@ exports = module.exports = Node; /***/ }), -/* 759 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87155,7 +87206,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -87180,7 +87231,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 760 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87193,9 +87244,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(761); -var isAccessor = __webpack_require__(762); -var isData = __webpack_require__(764); +var typeOf = __webpack_require__(762); +var isAccessor = __webpack_require__(763); +var isData = __webpack_require__(765); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -87209,7 +87260,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 761 */ +/* 762 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87344,7 +87395,7 @@ function isBuffer(val) { /***/ }), -/* 762 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87357,7 +87408,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(763); +var typeOf = __webpack_require__(764); // accessor descriptor properties var accessor = { @@ -87420,7 +87471,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 763 */ +/* 764 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87555,7 +87606,7 @@ function isBuffer(val) { /***/ }), -/* 764 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87568,7 +87619,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(765); +var typeOf = __webpack_require__(766); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -87611,7 +87662,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 765 */ +/* 766 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87746,13 +87797,13 @@ function isBuffer(val) { /***/ }), -/* 766 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(754); var utils = module.exports; /** @@ -88772,17 +88823,17 @@ function assert(val, message) { /***/ }), -/* 767 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(738); -var Snapdragon = __webpack_require__(768); -var compilers = __webpack_require__(742); -var parsers = __webpack_require__(757); -var utils = __webpack_require__(743); +var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(769); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(758); +var utils = __webpack_require__(744); /** * Customize Snapdragon parser and renderer @@ -88883,17 +88934,17 @@ module.exports = Braces; /***/ }), -/* 768 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(769); -var define = __webpack_require__(730); -var Compiler = __webpack_require__(798); -var Parser = __webpack_require__(827); -var utils = __webpack_require__(807); +var Base = __webpack_require__(770); +var define = __webpack_require__(731); +var Compiler = __webpack_require__(799); +var Parser = __webpack_require__(828); +var utils = __webpack_require__(808); var regexCache = {}; var cache = {}; @@ -89064,20 +89115,20 @@ module.exports.Parser = Parser; /***/ }), -/* 769 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(770); -var CacheBase = __webpack_require__(771); -var Emitter = __webpack_require__(772); -var isObject = __webpack_require__(748); -var merge = __webpack_require__(789); -var pascal = __webpack_require__(792); -var cu = __webpack_require__(793); +var define = __webpack_require__(771); +var CacheBase = __webpack_require__(772); +var Emitter = __webpack_require__(773); +var isObject = __webpack_require__(749); +var merge = __webpack_require__(790); +var pascal = __webpack_require__(793); +var cu = __webpack_require__(794); /** * Optionally define a custom `cache` namespace to use. @@ -89506,7 +89557,7 @@ module.exports.namespace = namespace; /***/ }), -/* 770 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89519,7 +89570,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -89544,21 +89595,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 771 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(748); -var Emitter = __webpack_require__(772); -var visit = __webpack_require__(773); -var toPath = __webpack_require__(776); -var union = __webpack_require__(777); -var del = __webpack_require__(781); -var get = __webpack_require__(779); -var has = __webpack_require__(786); -var set = __webpack_require__(780); +var isObject = __webpack_require__(749); +var Emitter = __webpack_require__(773); +var visit = __webpack_require__(774); +var toPath = __webpack_require__(777); +var union = __webpack_require__(778); +var del = __webpack_require__(782); +var get = __webpack_require__(780); +var has = __webpack_require__(787); +var set = __webpack_require__(781); /** * Create a `Cache` constructor that when instantiated will @@ -89812,7 +89863,7 @@ module.exports.namespace = namespace; /***/ }), -/* 772 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { @@ -89981,7 +90032,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 773 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89994,8 +90045,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(774); -var mapVisit = __webpack_require__(775); +var visit = __webpack_require__(775); +var mapVisit = __webpack_require__(776); module.exports = function(collection, method, val) { var result; @@ -90018,7 +90069,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 774 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90031,7 +90082,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(749); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -90058,14 +90109,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 775 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(774); +var visit = __webpack_require__(775); /** * Map `visit` over an array of objects. @@ -90102,7 +90153,7 @@ function isObject(val) { /***/ }), -/* 776 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90115,7 +90166,7 @@ function isObject(val) { -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(754); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -90142,16 +90193,16 @@ function filter(arr) { /***/ }), -/* 777 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(739); -var union = __webpack_require__(778); -var get = __webpack_require__(779); -var set = __webpack_require__(780); +var isObject = __webpack_require__(740); +var union = __webpack_require__(779); +var get = __webpack_require__(780); +var set = __webpack_require__(781); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -90179,7 +90230,7 @@ function arrayify(val) { /***/ }), -/* 778 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90215,7 +90266,7 @@ module.exports = function union(init) { /***/ }), -/* 779 */ +/* 780 */ /***/ (function(module, exports) { /*! @@ -90271,7 +90322,7 @@ function toString(val) { /***/ }), -/* 780 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90284,10 +90335,10 @@ function toString(val) { -var split = __webpack_require__(744); -var extend = __webpack_require__(738); -var isPlainObject = __webpack_require__(747); -var isObject = __webpack_require__(739); +var split = __webpack_require__(745); +var extend = __webpack_require__(739); +var isPlainObject = __webpack_require__(748); +var isObject = __webpack_require__(740); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -90333,7 +90384,7 @@ function isValidKey(key) { /***/ }), -/* 781 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90346,8 +90397,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(748); -var has = __webpack_require__(782); +var isObject = __webpack_require__(749); +var has = __webpack_require__(783); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -90372,7 +90423,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 782 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90385,9 +90436,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(783); -var hasValues = __webpack_require__(785); -var get = __webpack_require__(779); +var isObject = __webpack_require__(784); +var hasValues = __webpack_require__(786); +var get = __webpack_require__(780); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -90398,7 +90449,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 783 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90411,7 +90462,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(784); +var isArray = __webpack_require__(785); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -90419,7 +90470,7 @@ module.exports = function isObject(val) { /***/ }), -/* 784 */ +/* 785 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -90430,7 +90481,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 785 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90473,7 +90524,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 786 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90486,9 +90537,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(748); -var hasValues = __webpack_require__(787); -var get = __webpack_require__(779); +var isObject = __webpack_require__(749); +var hasValues = __webpack_require__(788); +var get = __webpack_require__(780); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -90496,7 +90547,7 @@ module.exports = function(val, prop) { /***/ }), -/* 787 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90509,8 +90560,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(788); -var isNumber = __webpack_require__(752); +var typeOf = __webpack_require__(789); +var isNumber = __webpack_require__(753); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -90563,10 +90614,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 788 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -90688,14 +90739,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 789 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(790); -var forIn = __webpack_require__(791); +var isExtendable = __webpack_require__(791); +var forIn = __webpack_require__(792); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -90759,7 +90810,7 @@ module.exports = mixinDeep; /***/ }), -/* 790 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90772,7 +90823,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -90780,7 +90831,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 791 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90803,7 +90854,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 792 */ +/* 793 */ /***/ (function(module, exports) { /*! @@ -90830,14 +90881,14 @@ module.exports = pascalcase; /***/ }), -/* 793 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(794); +var utils = __webpack_require__(795); /** * Expose class utils @@ -91202,7 +91253,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 794 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91216,10 +91267,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(778); -utils.define = __webpack_require__(730); -utils.isObj = __webpack_require__(748); -utils.staticExtend = __webpack_require__(795); +utils.union = __webpack_require__(779); +utils.define = __webpack_require__(731); +utils.isObj = __webpack_require__(749); +utils.staticExtend = __webpack_require__(796); /** @@ -91230,7 +91281,7 @@ module.exports = utils; /***/ }), -/* 795 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91243,8 +91294,8 @@ module.exports = utils; -var copy = __webpack_require__(796); -var define = __webpack_require__(730); +var copy = __webpack_require__(797); +var define = __webpack_require__(731); var util = __webpack_require__(29); /** @@ -91327,15 +91378,15 @@ module.exports = extend; /***/ }), -/* 796 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(753); -var copyDescriptor = __webpack_require__(797); -var define = __webpack_require__(730); +var typeOf = __webpack_require__(754); +var copyDescriptor = __webpack_require__(798); +var define = __webpack_require__(731); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -91508,7 +91559,7 @@ module.exports.has = has; /***/ }), -/* 797 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91596,16 +91647,16 @@ function isObject(val) { /***/ }), -/* 798 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(799); -var define = __webpack_require__(730); -var debug = __webpack_require__(801)('snapdragon:compiler'); -var utils = __webpack_require__(807); +var use = __webpack_require__(800); +var define = __webpack_require__(731); +var debug = __webpack_require__(802)('snapdragon:compiler'); +var utils = __webpack_require__(808); /** * Create a new `Compiler` with the given `options`. @@ -91759,7 +91810,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(826); + var sourcemaps = __webpack_require__(827); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -91780,7 +91831,7 @@ module.exports = Compiler; /***/ }), -/* 799 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91793,7 +91844,7 @@ module.exports = Compiler; -var utils = __webpack_require__(800); +var utils = __webpack_require__(801); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -91908,7 +91959,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 800 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91922,8 +91973,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(730); -utils.isObject = __webpack_require__(748); +utils.define = __webpack_require__(731); +utils.isObject = __webpack_require__(749); utils.isString = function(val) { @@ -91938,7 +91989,7 @@ module.exports = utils; /***/ }), -/* 801 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91947,14 +91998,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(802); + module.exports = __webpack_require__(803); } else { - module.exports = __webpack_require__(805); + module.exports = __webpack_require__(806); } /***/ }), -/* 802 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91963,7 +92014,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(803); +exports = module.exports = __webpack_require__(804); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -92145,7 +92196,7 @@ function localstorage() { /***/ }), -/* 803 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { @@ -92161,7 +92212,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(804); +exports.humanize = __webpack_require__(805); /** * The currently active debug mode names, and names to skip. @@ -92353,7 +92404,7 @@ function coerce(val) { /***/ }), -/* 804 */ +/* 805 */ /***/ (function(module, exports) { /** @@ -92511,7 +92562,7 @@ function plural(ms, n, name) { /***/ }), -/* 805 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -92527,7 +92578,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(803); +exports = module.exports = __webpack_require__(804); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -92706,7 +92757,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(806); + var net = __webpack_require__(807); stream = new net.Socket({ fd: fd, readable: false, @@ -92765,13 +92816,13 @@ exports.enable(load()); /***/ }), -/* 806 */ +/* 807 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 807 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92781,9 +92832,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(738); -exports.SourceMap = __webpack_require__(808); -exports.sourceMapResolve = __webpack_require__(819); +exports.extend = __webpack_require__(739); +exports.SourceMap = __webpack_require__(809); +exports.sourceMapResolve = __webpack_require__(820); /** * Convert backslash in the given string to forward slashes @@ -92826,7 +92877,7 @@ exports.last = function(arr, n) { /***/ }), -/* 808 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -92834,13 +92885,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(815).SourceMapConsumer; -exports.SourceNode = __webpack_require__(818).SourceNode; +exports.SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(816).SourceMapConsumer; +exports.SourceNode = __webpack_require__(819).SourceNode; /***/ }), -/* 809 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -92850,10 +92901,10 @@ exports.SourceNode = __webpack_require__(818).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(810); -var util = __webpack_require__(812); -var ArraySet = __webpack_require__(813).ArraySet; -var MappingList = __webpack_require__(814).MappingList; +var base64VLQ = __webpack_require__(811); +var util = __webpack_require__(813); +var ArraySet = __webpack_require__(814).ArraySet; +var MappingList = __webpack_require__(815).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -93262,7 +93313,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 810 */ +/* 811 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93302,7 +93353,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(811); +var base64 = __webpack_require__(812); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -93408,7 +93459,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 811 */ +/* 812 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93481,7 +93532,7 @@ exports.decode = function (charCode) { /***/ }), -/* 812 */ +/* 813 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93904,7 +93955,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 813 */ +/* 814 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93914,7 +93965,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); +var util = __webpack_require__(813); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -94031,7 +94082,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 814 */ +/* 815 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94041,7 +94092,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); +var util = __webpack_require__(813); /** * Determine whether mappingB is after mappingA with respect to generated @@ -94116,7 +94167,7 @@ exports.MappingList = MappingList; /***/ }), -/* 815 */ +/* 816 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94126,11 +94177,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); -var binarySearch = __webpack_require__(816); -var ArraySet = __webpack_require__(813).ArraySet; -var base64VLQ = __webpack_require__(810); -var quickSort = __webpack_require__(817).quickSort; +var util = __webpack_require__(813); +var binarySearch = __webpack_require__(817); +var ArraySet = __webpack_require__(814).ArraySet; +var base64VLQ = __webpack_require__(811); +var quickSort = __webpack_require__(818).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -95204,7 +95255,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 816 */ +/* 817 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95321,7 +95372,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 817 */ +/* 818 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95441,7 +95492,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 818 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95451,8 +95502,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; -var util = __webpack_require__(812); +var SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; +var util = __webpack_require__(813); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -95860,17 +95911,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 819 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(820) -var resolveUrl = __webpack_require__(821) -var decodeUriComponent = __webpack_require__(822) -var urix = __webpack_require__(824) -var atob = __webpack_require__(825) +var sourceMappingURL = __webpack_require__(821) +var resolveUrl = __webpack_require__(822) +var decodeUriComponent = __webpack_require__(823) +var urix = __webpack_require__(825) +var atob = __webpack_require__(826) @@ -96168,7 +96219,7 @@ module.exports = { /***/ }), -/* 820 */ +/* 821 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -96231,7 +96282,7 @@ void (function(root, factory) { /***/ }), -/* 821 */ +/* 822 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96249,13 +96300,13 @@ module.exports = resolveUrl /***/ }), -/* 822 */ +/* 823 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(823) +var decodeUriComponent = __webpack_require__(824) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -96266,7 +96317,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 823 */ +/* 824 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96367,7 +96418,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 824 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96390,7 +96441,7 @@ module.exports = urix /***/ }), -/* 825 */ +/* 826 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96404,7 +96455,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 826 */ +/* 827 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96412,8 +96463,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(730); -var utils = __webpack_require__(807); +var define = __webpack_require__(731); +var utils = __webpack_require__(808); /** * Expose `mixin()`. @@ -96556,19 +96607,19 @@ exports.comment = function(node) { /***/ }), -/* 827 */ +/* 828 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(799); +var use = __webpack_require__(800); var util = __webpack_require__(29); -var Cache = __webpack_require__(828); -var define = __webpack_require__(730); -var debug = __webpack_require__(801)('snapdragon:parser'); -var Position = __webpack_require__(829); -var utils = __webpack_require__(807); +var Cache = __webpack_require__(829); +var define = __webpack_require__(731); +var debug = __webpack_require__(802)('snapdragon:parser'); +var Position = __webpack_require__(830); +var utils = __webpack_require__(808); /** * Create a new `Parser` with the given `input` and `options`. @@ -97096,7 +97147,7 @@ module.exports = Parser; /***/ }), -/* 828 */ +/* 829 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97203,13 +97254,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 829 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(730); +var define = __webpack_require__(731); /** * Store position for a node @@ -97224,16 +97275,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 830 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(831); -var define = __webpack_require__(837); -var extend = __webpack_require__(838); -var not = __webpack_require__(840); +var safe = __webpack_require__(832); +var define = __webpack_require__(838); +var extend = __webpack_require__(839); +var not = __webpack_require__(841); var MAX_LENGTH = 1024 * 64; /** @@ -97386,10 +97437,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 831 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(832); +var parse = __webpack_require__(833); var types = parse.types; module.exports = function (re, opts) { @@ -97435,13 +97486,13 @@ function isRegExp (x) { /***/ }), -/* 832 */ +/* 833 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(833); -var types = __webpack_require__(834); -var sets = __webpack_require__(835); -var positions = __webpack_require__(836); +var util = __webpack_require__(834); +var types = __webpack_require__(835); +var sets = __webpack_require__(836); +var positions = __webpack_require__(837); module.exports = function(regexpStr) { @@ -97723,11 +97774,11 @@ module.exports.types = types; /***/ }), -/* 833 */ +/* 834 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); -var sets = __webpack_require__(835); +var types = __webpack_require__(835); +var sets = __webpack_require__(836); // All of these are private and only used by randexp. @@ -97840,7 +97891,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 834 */ +/* 835 */ /***/ (function(module, exports) { module.exports = { @@ -97856,10 +97907,10 @@ module.exports = { /***/ }), -/* 835 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); +var types = __webpack_require__(835); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -97944,10 +97995,10 @@ exports.anyChar = function() { /***/ }), -/* 836 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); +var types = __webpack_require__(835); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -97967,7 +98018,7 @@ exports.end = function() { /***/ }), -/* 837 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97980,8 +98031,8 @@ exports.end = function() { -var isobject = __webpack_require__(748); -var isDescriptor = __webpack_require__(760); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(761); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -98012,14 +98063,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 838 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(839); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(840); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -98079,7 +98130,7 @@ function isEnum(obj, key) { /***/ }), -/* 839 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98092,7 +98143,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -98100,14 +98151,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 840 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(838); -var safe = __webpack_require__(831); +var extend = __webpack_require__(839); +var safe = __webpack_require__(832); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -98179,14 +98230,14 @@ module.exports = toRegex; /***/ }), -/* 841 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(842); -var extglob = __webpack_require__(857); +var nanomatch = __webpack_require__(843); +var extglob = __webpack_require__(858); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -98263,7 +98314,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 842 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98274,17 +98325,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(729); -var extend = __webpack_require__(843); +var toRegex = __webpack_require__(730); +var extend = __webpack_require__(844); /** * Local dependencies */ -var compilers = __webpack_require__(845); -var parsers = __webpack_require__(846); -var cache = __webpack_require__(849); -var utils = __webpack_require__(851); +var compilers = __webpack_require__(846); +var parsers = __webpack_require__(847); +var cache = __webpack_require__(850); +var utils = __webpack_require__(852); var MAX_LENGTH = 1024 * 64; /** @@ -99108,14 +99159,14 @@ module.exports = nanomatch; /***/ }), -/* 843 */ +/* 844 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(844); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(845); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -99175,7 +99226,7 @@ function isEnum(obj, key) { /***/ }), -/* 844 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99188,7 +99239,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -99196,7 +99247,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 845 */ +/* 846 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99542,15 +99593,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 846 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(740); -var toRegex = __webpack_require__(729); -var isOdd = __webpack_require__(847); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(730); +var isOdd = __webpack_require__(848); /** * Characters to use in negation regex (we want to "not" match @@ -99936,7 +99987,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 847 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99949,7 +100000,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(848); +var isNumber = __webpack_require__(849); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -99963,7 +100014,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 848 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99991,14 +100042,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 849 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(850))(); +module.exports = new (__webpack_require__(851))(); /***/ }), -/* 850 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100011,7 +100062,7 @@ module.exports = new (__webpack_require__(850))(); -var MapCache = __webpack_require__(828); +var MapCache = __webpack_require__(829); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -100133,7 +100184,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 851 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100146,14 +100197,14 @@ var path = __webpack_require__(16); * Module dependencies */ -var isWindows = __webpack_require__(852)(); -var Snapdragon = __webpack_require__(768); -utils.define = __webpack_require__(853); -utils.diff = __webpack_require__(854); -utils.extend = __webpack_require__(843); -utils.pick = __webpack_require__(855); -utils.typeOf = __webpack_require__(856); -utils.unique = __webpack_require__(741); +var isWindows = __webpack_require__(853)(); +var Snapdragon = __webpack_require__(769); +utils.define = __webpack_require__(854); +utils.diff = __webpack_require__(855); +utils.extend = __webpack_require__(844); +utils.pick = __webpack_require__(856); +utils.typeOf = __webpack_require__(857); +utils.unique = __webpack_require__(742); /** * Returns true if the given value is effectively an empty string @@ -100519,7 +100570,7 @@ utils.unixify = function(options) { /***/ }), -/* 852 */ +/* 853 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -100547,7 +100598,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 853 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100560,8 +100611,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(748); -var isDescriptor = __webpack_require__(760); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(761); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -100592,7 +100643,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 854 */ +/* 855 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100646,7 +100697,7 @@ function diffArray(one, two) { /***/ }), -/* 855 */ +/* 856 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100659,7 +100710,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(749); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -100688,7 +100739,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 856 */ +/* 857 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -100823,7 +100874,7 @@ function isBuffer(val) { /***/ }), -/* 857 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100833,18 +100884,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(738); -var unique = __webpack_require__(741); -var toRegex = __webpack_require__(729); +var extend = __webpack_require__(739); +var unique = __webpack_require__(742); +var toRegex = __webpack_require__(730); /** * Local dependencies */ -var compilers = __webpack_require__(858); -var parsers = __webpack_require__(869); -var Extglob = __webpack_require__(872); -var utils = __webpack_require__(871); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(870); +var Extglob = __webpack_require__(873); +var utils = __webpack_require__(872); var MAX_LENGTH = 1024 * 64; /** @@ -101161,13 +101212,13 @@ module.exports = extglob; /***/ }), -/* 858 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(859); +var brackets = __webpack_require__(860); /** * Extglob compilers @@ -101337,7 +101388,7 @@ module.exports = function(extglob) { /***/ }), -/* 859 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101347,17 +101398,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(860); -var parsers = __webpack_require__(862); +var compilers = __webpack_require__(861); +var parsers = __webpack_require__(863); /** * Module dependencies */ -var debug = __webpack_require__(864)('expand-brackets'); -var extend = __webpack_require__(738); -var Snapdragon = __webpack_require__(768); -var toRegex = __webpack_require__(729); +var debug = __webpack_require__(865)('expand-brackets'); +var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(769); +var toRegex = __webpack_require__(730); /** * Parses the given POSIX character class `pattern` and returns a @@ -101555,13 +101606,13 @@ module.exports = brackets; /***/ }), -/* 860 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(861); +var posix = __webpack_require__(862); module.exports = function(brackets) { brackets.compiler @@ -101649,7 +101700,7 @@ module.exports = function(brackets) { /***/ }), -/* 861 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101678,14 +101729,14 @@ module.exports = { /***/ }), -/* 862 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(863); -var define = __webpack_require__(730); +var utils = __webpack_require__(864); +var define = __webpack_require__(731); /** * Text regex @@ -101904,14 +101955,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 863 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(729); -var regexNot = __webpack_require__(740); +var toRegex = __webpack_require__(730); +var regexNot = __webpack_require__(741); var cached; /** @@ -101945,7 +101996,7 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 864 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -101954,14 +102005,14 @@ exports.createRegex = function(pattern, include) { */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(865); + module.exports = __webpack_require__(866); } else { - module.exports = __webpack_require__(868); + module.exports = __webpack_require__(869); } /***/ }), -/* 865 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -101970,7 +102021,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(866); +exports = module.exports = __webpack_require__(867); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -102152,7 +102203,7 @@ function localstorage() { /***/ }), -/* 866 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { @@ -102168,7 +102219,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(867); +exports.humanize = __webpack_require__(868); /** * The currently active debug mode names, and names to skip. @@ -102360,7 +102411,7 @@ function coerce(val) { /***/ }), -/* 867 */ +/* 868 */ /***/ (function(module, exports) { /** @@ -102518,7 +102569,7 @@ function plural(ms, n, name) { /***/ }), -/* 868 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -102534,7 +102585,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(866); +exports = module.exports = __webpack_require__(867); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -102713,7 +102764,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(806); + var net = __webpack_require__(807); stream = new net.Socket({ fd: fd, readable: false, @@ -102772,15 +102823,15 @@ exports.enable(load()); /***/ }), -/* 869 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(859); -var define = __webpack_require__(870); -var utils = __webpack_require__(871); +var brackets = __webpack_require__(860); +var define = __webpack_require__(871); +var utils = __webpack_require__(872); /** * Characters to use in text regex (we want to "not" match @@ -102935,7 +102986,7 @@ module.exports = parsers; /***/ }), -/* 870 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102948,7 +102999,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -102973,14 +103024,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 871 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(740); -var Cache = __webpack_require__(850); +var regex = __webpack_require__(741); +var Cache = __webpack_require__(851); /** * Utils @@ -103049,7 +103100,7 @@ utils.createRegex = function(str) { /***/ }), -/* 872 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103059,16 +103110,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(768); -var define = __webpack_require__(870); -var extend = __webpack_require__(738); +var Snapdragon = __webpack_require__(769); +var define = __webpack_require__(871); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(858); -var parsers = __webpack_require__(869); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(870); /** * Customize Snapdragon parser and renderer @@ -103134,16 +103185,16 @@ module.exports = Extglob; /***/ }), -/* 873 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(857); -var nanomatch = __webpack_require__(842); -var regexNot = __webpack_require__(740); -var toRegex = __webpack_require__(830); +var extglob = __webpack_require__(858); +var nanomatch = __webpack_require__(843); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(831); var not; /** @@ -103224,14 +103275,14 @@ function textRegex(pattern) { /***/ }), -/* 874 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(850))(); +module.exports = new (__webpack_require__(851))(); /***/ }), -/* 875 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103244,13 +103295,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(768); -utils.define = __webpack_require__(837); -utils.diff = __webpack_require__(854); -utils.extend = __webpack_require__(838); -utils.pick = __webpack_require__(855); -utils.typeOf = __webpack_require__(876); -utils.unique = __webpack_require__(741); +var Snapdragon = __webpack_require__(769); +utils.define = __webpack_require__(838); +utils.diff = __webpack_require__(855); +utils.extend = __webpack_require__(839); +utils.pick = __webpack_require__(856); +utils.typeOf = __webpack_require__(877); +utils.unique = __webpack_require__(742); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -103547,7 +103598,7 @@ utils.unixify = function(options) { /***/ }), -/* 876 */ +/* 877 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -103682,7 +103733,7 @@ function isBuffer(val) { /***/ }), -/* 877 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103701,9 +103752,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_stream_1 = __webpack_require__(895); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_stream_1 = __webpack_require__(896); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -103764,15 +103815,15 @@ exports.default = ReaderAsync; /***/ }), -/* 878 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(879); -const readdirAsync = __webpack_require__(887); -const readdirStream = __webpack_require__(890); +const readdirSync = __webpack_require__(880); +const readdirAsync = __webpack_require__(888); +const readdirStream = __webpack_require__(891); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -103856,7 +103907,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 879 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103864,11 +103915,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(880); +const DirectoryReader = __webpack_require__(881); let syncFacade = { - fs: __webpack_require__(885), - forEach: __webpack_require__(886), + fs: __webpack_require__(886), + forEach: __webpack_require__(887), sync: true }; @@ -103897,7 +103948,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 880 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103906,9 +103957,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(881); -const stat = __webpack_require__(883); -const call = __webpack_require__(884); +const normalizeOptions = __webpack_require__(882); +const stat = __webpack_require__(884); +const call = __webpack_require__(885); /** * Asynchronously reads the contents of a directory and streams the results @@ -104284,14 +104335,14 @@ module.exports = DirectoryReader; /***/ }), -/* 881 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(882); +const globToRegExp = __webpack_require__(883); module.exports = normalizeOptions; @@ -104468,7 +104519,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 882 */ +/* 883 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -104605,13 +104656,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 883 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(884); +const call = __webpack_require__(885); module.exports = stat; @@ -104686,7 +104737,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 884 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104747,14 +104798,14 @@ function callOnce (fn) { /***/ }), -/* 885 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(884); +const call = __webpack_require__(885); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -104818,7 +104869,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 886 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104847,7 +104898,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 887 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104855,12 +104906,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(888); -const DirectoryReader = __webpack_require__(880); +const maybe = __webpack_require__(889); +const DirectoryReader = __webpack_require__(881); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(889), + forEach: __webpack_require__(890), async: true }; @@ -104902,7 +104953,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 888 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104929,7 +104980,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 889 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104965,7 +105016,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 890 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104973,11 +105024,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(880); +const DirectoryReader = __webpack_require__(881); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(889), + forEach: __webpack_require__(890), async: true }; @@ -104997,16 +105048,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 891 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(892); -var entry_1 = __webpack_require__(894); -var pathUtil = __webpack_require__(893); +var deep_1 = __webpack_require__(893); +var entry_1 = __webpack_require__(895); +var pathUtil = __webpack_require__(894); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -105072,14 +105123,14 @@ exports.default = Reader; /***/ }), -/* 892 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(893); -var patternUtils = __webpack_require__(722); +var pathUtils = __webpack_require__(894); +var patternUtils = __webpack_require__(723); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -105162,7 +105213,7 @@ exports.default = DeepFilter; /***/ }), -/* 893 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105193,14 +105244,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 894 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(893); -var patternUtils = __webpack_require__(722); +var pathUtils = __webpack_require__(894); +var patternUtils = __webpack_require__(723); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -105285,7 +105336,7 @@ exports.default = EntryFilter; /***/ }), -/* 895 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105305,8 +105356,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(896); -var fs_1 = __webpack_require__(900); +var fsStat = __webpack_require__(897); +var fs_1 = __webpack_require__(901); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -105356,14 +105407,14 @@ exports.default = FileSystemStream; /***/ }), -/* 896 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(897); -const statProvider = __webpack_require__(899); +const optionsManager = __webpack_require__(898); +const statProvider = __webpack_require__(900); /** * Asynchronous API. */ @@ -105394,13 +105445,13 @@ exports.statSync = statSync; /***/ }), -/* 897 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(898); +const fsAdapter = __webpack_require__(899); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -105413,7 +105464,7 @@ exports.prepare = prepare; /***/ }), -/* 898 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105436,7 +105487,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 899 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105488,7 +105539,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 900 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105519,7 +105570,7 @@ exports.default = FileSystem; /***/ }), -/* 901 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105539,9 +105590,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_stream_1 = __webpack_require__(895); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_stream_1 = __webpack_require__(896); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -105609,7 +105660,7 @@ exports.default = ReaderStream; /***/ }), -/* 902 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105628,9 +105679,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_sync_1 = __webpack_require__(903); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_sync_1 = __webpack_require__(904); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -105690,7 +105741,7 @@ exports.default = ReaderSync; /***/ }), -/* 903 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105709,8 +105760,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(896); -var fs_1 = __webpack_require__(900); +var fsStat = __webpack_require__(897); +var fs_1 = __webpack_require__(901); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -105756,7 +105807,7 @@ exports.default = FileSystemSync; /***/ }), -/* 904 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105772,7 +105823,7 @@ exports.flatten = flatten; /***/ }), -/* 905 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105793,13 +105844,13 @@ exports.merge = merge; /***/ }), -/* 906 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(907); +const pathType = __webpack_require__(908); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -105865,13 +105916,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 907 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(908); +const pify = __webpack_require__(909); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -105914,7 +105965,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 908 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106005,17 +106056,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 909 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(718); -const gitIgnore = __webpack_require__(910); -const pify = __webpack_require__(911); -const slash = __webpack_require__(912); +const fastGlob = __webpack_require__(719); +const gitIgnore = __webpack_require__(911); +const pify = __webpack_require__(912); +const slash = __webpack_require__(913); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -106113,7 +106164,7 @@ module.exports.sync = options => { /***/ }), -/* 910 */ +/* 911 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -106582,7 +106633,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 911 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106657,7 +106708,7 @@ module.exports = (input, options) => { /***/ }), -/* 912 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106675,17 +106726,17 @@ module.exports = input => { /***/ }), -/* 913 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(914); -const CpFileError = __webpack_require__(917); -const fs = __webpack_require__(921); -const ProgressEmitter = __webpack_require__(924); +const pEvent = __webpack_require__(915); +const CpFileError = __webpack_require__(918); +const fs = __webpack_require__(922); +const ProgressEmitter = __webpack_require__(925); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -106799,12 +106850,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 914 */ +/* 915 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(915); +const pTimeout = __webpack_require__(916); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -107095,12 +107146,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 915 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(916); +const pFinally = __webpack_require__(917); class TimeoutError extends Error { constructor(message) { @@ -107146,7 +107197,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 916 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107168,12 +107219,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 917 */ +/* 918 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(918); +const NestedError = __webpack_require__(919); class CpFileError extends NestedError { constructor(message, nested) { @@ -107187,10 +107238,10 @@ module.exports = CpFileError; /***/ }), -/* 918 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(919); +var inherits = __webpack_require__(920); var NestedError = function (message, nested) { this.nested = nested; @@ -107241,7 +107292,7 @@ module.exports = NestedError; /***/ }), -/* 919 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -107249,12 +107300,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(920); + module.exports = __webpack_require__(921); } /***/ }), -/* 920 */ +/* 921 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -107283,16 +107334,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 921 */ +/* 922 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(922); -const pEvent = __webpack_require__(914); -const CpFileError = __webpack_require__(917); +const makeDir = __webpack_require__(923); +const pEvent = __webpack_require__(915); +const CpFileError = __webpack_require__(918); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -107389,7 +107440,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 922 */ +/* 923 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107397,7 +107448,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const {promisify} = __webpack_require__(29); -const semver = __webpack_require__(923); +const semver = __webpack_require__(924); const defaults = { mode: 0o777 & (~process.umask()), @@ -107546,7 +107597,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 923 */ +/* 924 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -109148,7 +109199,7 @@ function coerce (version, options) { /***/ }), -/* 924 */ +/* 925 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109189,7 +109240,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 925 */ +/* 926 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109235,12 +109286,12 @@ exports.default = module.exports; /***/ }), -/* 926 */ +/* 927 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(927); +const NestedError = __webpack_require__(928); class CpyError extends NestedError { constructor(message, nested) { @@ -109254,7 +109305,7 @@ module.exports = CpyError; /***/ }), -/* 927 */ +/* 928 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -109310,7 +109361,7 @@ module.exports = NestedError; /***/ }), -/* 928 */ +/* 929 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 444d46307b059..a05e1634226e5 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -26,7 +26,7 @@ "@types/lodash.clonedeepwith": "^4.5.3", "@types/log-symbols": "^2.0.0", "@types/ncp": "^2.0.1", - "@types/node": "^10.12.27", + "@types/node": ">=10.17.17 <10.20.0", "@types/ora": "^1.3.5", "@types/read-pkg": "^4.0.0", "@types/strip-ansi": "^3.0.0", @@ -48,6 +48,7 @@ "globby": "^8.0.1", "has-ansi": "^3.0.0", "indent-string": "^3.2.0", + "is-path-inside": "^3.0.2", "lodash.clonedeepwith": "^4.5.0", "log-symbols": "^2.2.0", "multimatch": "^4.0.0", diff --git a/packages/kbn-pm/src/utils/kibana.ts b/packages/kbn-pm/src/utils/kibana.ts index 36f697d19fc1f..58af98b2a92db 100644 --- a/packages/kbn-pm/src/utils/kibana.ts +++ b/packages/kbn-pm/src/utils/kibana.ts @@ -20,6 +20,7 @@ import Path from 'path'; import multimatch from 'multimatch'; +import isPathInside from 'is-path-inside'; import { ProjectMap, getProjects, includeTransitiveProjects } from './projects'; import { Project } from './project'; @@ -121,4 +122,15 @@ export class Kibana { return filteredProjects; } + + isPartOfRepo(project: Project) { + return ( + project.path === this.kibanaProject.path || + isPathInside(project.path, this.kibanaProject.path) + ); + } + + isOutsideRepo(project: Project) { + return !this.isPartOfRepo(project); + } } diff --git a/packages/kbn-pm/src/utils/project_checksums.ts b/packages/kbn-pm/src/utils/project_checksums.ts index 2fd24c8fc9577..572f2adb19bd9 100644 --- a/packages/kbn-pm/src/utils/project_checksums.ts +++ b/packages/kbn-pm/src/utils/project_checksums.ts @@ -43,7 +43,14 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too const { stdout } = await execa( 'git', - ['ls-files', '-dmt', '--', ...Array.from(projects.values()).map(p => p.path)], + [ + 'ls-files', + '-dmt', + '--', + ...Array.from(projects.values()) + .filter(p => kbn.isPartOfRepo(p)) + .map(p => p.path), + ], { cwd: kbn.getAbsolute(), } @@ -84,9 +91,14 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too } const sortedRelevantProjects = Array.from(projects.values()).sort(projectBySpecificitySorter); - const changesByProject = new Map(); + const changesByProject = new Map(); for (const project of sortedRelevantProjects) { + if (kbn.isOutsideRepo(project)) { + changesByProject.set(project, undefined); + continue; + } + const ownChanges: Changes = new Map(); const prefix = kbn.getRelative(project.path); @@ -114,6 +126,10 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too /** Get the latest commit sha for a project */ async function getLatestSha(project: Project, kbn: Kibana) { + if (kbn.isOutsideRepo(project)) { + return; + } + const { stdout } = await execa( 'git', ['log', '-n', '1', '--pretty=format:%H', '--', project.path], @@ -175,7 +191,7 @@ function resolveDepsForProject(project: Project, yarnLock: YarnLock, kbn: Kibana */ async function getChecksum( project: Project, - changes: Changes, + changes: Changes | undefined, yarnLock: YarnLock, kbn: Kibana, log: ToolingLog @@ -185,7 +201,7 @@ async function getChecksum( log.verbose(`[${project.name}] local sha:`, sha); } - if (Array.from(changes.values()).includes('invalid')) { + if (!changes || Array.from(changes.values()).includes('invalid')) { log.warning(`[${project.name}] unable to determine local changes, caching disabled`); return; } @@ -248,7 +264,7 @@ export async function getAllChecksums(kbn: Kibana, log: ToolingLog) { Array.from(projects.values()).map(async project => { cacheKeys.set( project.name, - await getChecksum(project, changesByProject.get(project)!, yarnLock, kbn, log) + await getChecksum(project, changesByProject.get(project), yarnLock, kbn, log) ); }) ); diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index 11b9450f2af6e..276a51c3a6a99 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -18,6 +18,7 @@ */ import { resolve } from 'path'; +import { inspect } from 'util'; import { run, createFlagError, Flags } from '@kbn/dev-utils'; import { FunctionalTestRunner } from './functional_test_runner'; @@ -48,12 +49,15 @@ export function runFtrCli() { kbnTestServer: { installDir: parseInstallDir(flags), }, + suiteFiles: { + include: toArray(flags.include as string | string[]).map(makeAbsolutePath), + exclude: toArray(flags.exclude as string | string[]).map(makeAbsolutePath), + }, suiteTags: { include: toArray(flags['include-tag'] as string | string[]), exclude: toArray(flags['exclude-tag'] as string | string[]), }, updateBaselines: flags.updateBaselines, - excludeTestFiles: flags.exclude || undefined, } ); @@ -83,7 +87,11 @@ export function runFtrCli() { } }; - process.on('unhandledRejection', err => teardown(err)); + process.on('unhandledRejection', err => + teardown( + err instanceof Error ? err : new Error(`non-Error type rejection value: ${inspect(err)}`) + ) + ); process.on('SIGTERM', () => teardown()); process.on('SIGINT', () => teardown()); @@ -104,7 +112,15 @@ export function runFtrCli() { }, { flags: { - string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag', 'kibana-install-dir'], + string: [ + 'config', + 'grep', + 'include', + 'exclude', + 'include-tag', + 'exclude-tag', + 'kibana-install-dir', + ], boolean: ['bail', 'invert', 'test-stats', 'updateBaselines', 'throttle', 'headless'], default: { config: 'test/functional/config.js', @@ -115,7 +131,8 @@ export function runFtrCli() { --bail stop tests after the first failure --grep pattern used to select which tests to run --invert invert grep to exclude tests - --exclude=file path to a test file that should not be loaded + --include=file a test file to be included, pass multiple times for multiple files + --exclude=file a test file to be excluded, pass multiple times for multiple files --include-tag=tag a tag to be included, pass multiple times for multiple tags --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags --test-stats print the number of tests (included and excluded) to STDERR diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 75623d6c08890..66f17ab579ec3 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -64,9 +64,16 @@ export const schema = Joi.object() testFiles: Joi.array().items(Joi.string()), testRunner: Joi.func(), - excludeTestFiles: Joi.array() - .items(Joi.string()) - .default([]), + suiteFiles: Joi.object() + .keys({ + include: Joi.array() + .items(Joi.string()) + .default([]), + exclude: Joi.array() + .items(Joi.string()) + .default([]), + }) + .default(), suiteTags: Joi.object() .keys({ @@ -248,5 +255,20 @@ export const schema = Joi.object() fixedHeaderHeight: Joi.number().default(50), }) .default(), + + // settings for the security service if there is no defaultRole defined, then default to superuser role. + security: Joi.object() + .keys({ + roles: Joi.object().default(), + defaultRoles: Joi.array() + .items(Joi.string()) + .when('$primary', { + is: true, + then: Joi.array().min(1), + }) + .default(['superuser']), + disableTestUser: Joi.boolean(), + }) + .default(), }) .default(); diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js index 64fc51a04aac9..1cac852a7e713 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ - +import { relative } from 'path'; +import { REPO_ROOT } from '@kbn/dev-utils'; import { createAssignmentProxy } from './assignment_proxy'; import { wrapFunction } from './wrap_function'; import { wrapRunnableArgs } from './wrap_runnable_args'; @@ -65,6 +66,10 @@ export function decorateMochaUi(lifecycle, context) { this._tags = [].concat(this._tags || [], tags); }; + const relativeFilePath = relative(REPO_ROOT, this.file); + this.tags(relativeFilePath); + this.suiteTag = relativeFilePath; // The tag that uniquely targets this suite/file + provider.call(this); after(async () => { diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js index 70b0c0874e5e9..6ee65b1b7e394 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js @@ -31,28 +31,12 @@ import { decorateMochaUi } from './decorate_mocha_ui'; * @param {String} path * @return {undefined} - mutates mocha, no return value */ -export const loadTestFiles = ({ - mocha, - log, - lifecycle, - providers, - paths, - excludePaths, - updateBaselines, -}) => { - const pendingExcludes = new Set(excludePaths.slice(0)); - +export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, updateBaselines }) => { const innerLoadTestFile = path => { if (typeof path !== 'string' || !isAbsolute(path)) { throw new TypeError('loadTestFile() only accepts absolute paths'); } - if (pendingExcludes.has(path)) { - pendingExcludes.delete(path); - log.warning('Skipping test file %s', path); - return; - } - loadTracer(path, `testFile[${path}]`, () => { log.verbose('Loading test file %s', path); @@ -94,13 +78,4 @@ export const loadTestFiles = ({ }; paths.forEach(innerLoadTestFile); - - if (pendingExcludes.size) { - throw new Error( - `After loading all test files some exclude paths were not consumed:${[ - '', - ...pendingExcludes, - ].join('\n -')}` - ); - } }; diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js index 326877919d985..61851cece0e8f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js @@ -18,6 +18,8 @@ */ import Mocha from 'mocha'; +import { relative } from 'path'; +import { REPO_ROOT } from '@kbn/dev-utils'; import { loadTestFiles } from './load_test_files'; import { filterSuitesByTags } from './filter_suites_by_tags'; @@ -50,10 +52,20 @@ export async function setupMocha(lifecycle, log, config, providers) { lifecycle, providers, paths: config.get('testFiles'), - excludePaths: config.get('excludeTestFiles'), updateBaselines: config.get('updateBaselines'), }); + // Each suite has a tag that is the path relative to the root of the repo + // So we just need to take input paths, make them relative to the root, and use them as tags + // Also, this is a separate filterSuitesByTags() call so that the test suites will be filtered first by + // files, then by tags. This way, you can target tags (like smoke) in a specific file. + filterSuitesByTags({ + log, + mocha, + include: config.get('suiteFiles.include').map(file => relative(REPO_ROOT, file)), + exclude: config.get('suiteFiles.exclude').map(file => relative(REPO_ROOT, file)), + }); + filterSuitesByTags({ log, mocha, diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap index bbf8b38712ac1..434c374d5d23d 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap @@ -16,6 +16,8 @@ Options: --bail Stop the test run at the first failure. --grep Pattern to select which tests to run. --updateBaselines Replace baseline screenshots with whatever is generated from the test. + --include Files that must included to be run, can be included multiple times. + --exclude Files that must NOT be included to be run, can be included multiple times. --include-tag Tags that suites must include to be run, can be included multiple times. --exclude-tag Tags that suites must NOT include to be run, can be included multiple times. --assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags. @@ -34,6 +36,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -52,6 +58,10 @@ Object { "debug": true, "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -69,6 +79,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -90,6 +104,10 @@ Object { "extraKbnOpts": Object { "server.foo": "bar", }, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -107,6 +125,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "quiet": true, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -124,6 +146,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "silent": true, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -140,6 +166,10 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -156,6 +186,10 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -173,6 +207,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "installDir": "foo", + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -190,6 +228,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "grep": "management", + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -206,6 +248,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -223,6 +269,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap index b12739b3b5df5..6ede71a6c3940 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap @@ -16,6 +16,8 @@ Options: --bail Stop the test run at the first failure. --grep Pattern to select which tests to run. --updateBaselines Replace baseline screenshots with whatever is generated from the test. + --include Files that must included to be run, can be included multiple times. + --exclude Files that must NOT be included to be run, can be included multiple times. --include-tag Tags that suites must include to be run, can be included multiple times. --exclude-tag Tags that suites must NOT include to be run, can be included multiple times. --assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags. diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js index b34006a38a45d..7d2414305de8e 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js @@ -46,6 +46,14 @@ const options = { updateBaselines: { desc: 'Replace baseline screenshots with whatever is generated from the test.', }, + include: { + arg: '', + desc: 'Files that must included to be run, can be included multiple times.', + }, + exclude: { + arg: '', + desc: 'Files that must NOT be included to be run, can be included multiple times.', + }, 'include-tag': { arg: '', desc: 'Tags that suites must include to be run, can be included multiple times.', @@ -115,6 +123,13 @@ export function processOptions(userOptions, defaultConfigPaths) { delete userOptions['kibana-install-dir']; } + userOptions.suiteFiles = { + include: [].concat(userOptions.include || []), + exclude: [].concat(userOptions.exclude || []), + }; + delete userOptions.include; + delete userOptions.exclude; + userOptions.suiteTags = { include: [].concat(userOptions['include-tag'] || []), exclude: [].concat(userOptions['exclude-tag'] || []), diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.js b/packages/kbn-test/src/functional_tests/lib/run_ftr.js index 9b631e33f3b24..14883ac977c43 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.js +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.js @@ -22,7 +22,7 @@ import { CliError } from './run_cli'; async function createFtr({ configPath, - options: { installDir, log, bail, grep, updateBaselines, suiteTags }, + options: { installDir, log, bail, grep, updateBaselines, suiteFiles, suiteTags }, }) { const config = await readConfigFile(log, configPath); @@ -37,6 +37,10 @@ async function createFtr({ installDir, }, updateBaselines, + suiteFiles: { + include: [...suiteFiles.include, ...config.get('suiteFiles.include')], + exclude: [...suiteFiles.exclude, ...config.get('suiteFiles.exclude')], + }, suiteTags: { include: [...suiteTags.include, ...config.get('suiteTags.include')], exclude: [...suiteTags.exclude, ...config.get('suiteTags.exclude')], diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index bd932c5961eca..89007461b63e6 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -61,8 +61,6 @@ const createStartContractMock = () => { getBrand$: jest.fn(), setIsVisible: jest.fn(), getIsVisible$: jest.fn(), - setIsCollapsed: jest.fn(), - getIsCollapsed$: jest.fn(), addApplicationClass: jest.fn(), removeApplicationClass: jest.fn(), getApplicationClasses$: jest.fn(), @@ -73,15 +71,16 @@ const createStartContractMock = () => { getHelpExtension$: jest.fn(), setHelpExtension: jest.fn(), setHelpSupportUrl: jest.fn(), + getIsNavDrawerLocked$: jest.fn(), }; startContract.navLinks.getAll.mockReturnValue([]); startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand)); startContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false)); - startContract.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false)); startContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name'])); startContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge)); startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb])); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); + startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); return startContract; }; diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 9018b21973634..bf531aaa00fac 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -259,40 +259,6 @@ describe('start', () => { }); }); - describe('is collapsed', () => { - it('updates/emits isCollapsed', async () => { - const { chrome, service } = await start(); - const promise = chrome - .getIsCollapsed$() - .pipe(toArray()) - .toPromise(); - - chrome.setIsCollapsed(true); - chrome.setIsCollapsed(false); - chrome.setIsCollapsed(true); - service.stop(); - - await expect(promise).resolves.toMatchInlineSnapshot(` - Array [ - false, - true, - false, - true, - ] - `); - }); - - it('only stores true in localStorage', async () => { - const { chrome } = await start(); - - chrome.setIsCollapsed(true); - expect(store.size).toBe(1); - - chrome.setIsCollapsed(false); - expect(store.size).toBe(0); - }); - }); - describe('application classes', () => { it('updates/emits the application classes', async () => { const { chrome, service } = await start(); @@ -442,12 +408,12 @@ describe('start', () => { }); describe('stop', () => { - it('completes applicationClass$, isCollapsed$, breadcrumbs$, isVisible$, and brand$ observables', async () => { + it('completes applicationClass$, getIsNavDrawerLocked, breadcrumbs$, isVisible$, and brand$ observables', async () => { const { chrome, service } = await start(); const promise = Rx.combineLatest( chrome.getBrand$(), chrome.getApplicationClasses$(), - chrome.getIsCollapsed$(), + chrome.getIsNavDrawerLocked$(), chrome.getBreadcrumbs$(), chrome.getIsVisible$(), chrome.getHelpExtension$() @@ -465,7 +431,7 @@ describe('stop', () => { Rx.combineLatest( chrome.getBrand$(), chrome.getApplicationClasses$(), - chrome.getIsCollapsed$(), + chrome.getIsNavDrawerLocked$(), chrome.getBreadcrumbs$(), chrome.getIsVisible$(), chrome.getHelpExtension$() diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 2b0b115ce068e..7c9b644b8b984 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -34,14 +34,14 @@ import { ChromeNavLinks, NavLinksService } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { NavControlsService, ChromeNavControls } from './nav_controls'; import { DocTitleService, ChromeDocTitle } from './doc_title'; -import { LoadingIndicator, HeaderWrapper as Header } from './ui'; +import { LoadingIndicator, Header } from './ui'; import { DocLinksStart } from '../doc_links'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; import { KIBANA_ASK_ELASTIC_LINK } from './constants'; import { IUiSettingsClient } from '../ui_settings'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; -const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; +const IS_LOCKED_KEY = 'core.chrome.isLocked'; /** @public */ export interface ChromeBadge { @@ -146,18 +146,25 @@ export class ChromeService { const appTitle$ = new BehaviorSubject('Kibana'); const brand$ = new BehaviorSubject({}); - const isCollapsed$ = new BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY)); const applicationClasses$ = new BehaviorSubject>(new Set()); const helpExtension$ = new BehaviorSubject(undefined); const breadcrumbs$ = new BehaviorSubject([]); const badge$ = new BehaviorSubject(undefined); const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK); + const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true'); const navControls = this.navControls.start(); const navLinks = this.navLinks.start({ application, http }); const recentlyAccessed = await this.recentlyAccessed.start({ http }); const docTitle = this.docTitle.start({ document: window.document }); + const setIsNavDrawerLocked = (isLocked: boolean) => { + isNavDrawerLocked$.next(isLocked); + localStorage.setItem(IS_LOCKED_KEY, `${isLocked}`); + }; + + const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$)); + if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { notifications.toasts.addWarning( i18n.translate('core.chrome.legacyBrowserWarning', { @@ -193,6 +200,8 @@ export class ChromeService { recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} navControlsRight$={navControls.getRight$()} + onIsLockedUpdate={setIsNavDrawerLocked} + isLocked$={getIsNavDrawerLocked$} /> ), @@ -214,17 +223,6 @@ export class ChromeService { setIsVisible: (isVisible: boolean) => this.toggleHidden$.next(!isVisible), - getIsCollapsed$: () => isCollapsed$.pipe(takeUntil(this.stop$)), - - setIsCollapsed: (isCollapsed: boolean) => { - isCollapsed$.next(isCollapsed); - if (isCollapsed) { - localStorage.setItem(IS_COLLAPSED_KEY, 'true'); - } else { - localStorage.removeItem(IS_COLLAPSED_KEY); - } - }, - getApplicationClasses$: () => applicationClasses$.pipe( map(set => [...set]), @@ -262,6 +260,8 @@ export class ChromeService { }, setHelpSupportUrl: (url: string) => helpSupportUrl$.next(url), + + getIsNavDrawerLocked$: () => getIsNavDrawerLocked$, }; } @@ -353,16 +353,6 @@ export interface ChromeStart { */ setIsVisible(isVisible: boolean): void; - /** - * Get an observable of the current collapsed state of the chrome. - */ - getIsCollapsed$(): Observable; - - /** - * Set the collapsed state of the chrome navigation. - */ - setIsCollapsed(isCollapsed: boolean): void; - /** * Get the current set of classNames that will be set on the application container. */ @@ -413,6 +403,11 @@ export interface ChromeStart { * @param url The updated support URL */ setHelpSupportUrl(url: string): void; + + /** + * Get an observable of the current locked state of the nav drawer. + */ + getIsNavDrawerLocked$(): Observable; } /** @internal */ diff --git a/src/core/public/chrome/ui/_loading_indicator.scss b/src/core/public/chrome/ui/_loading_indicator.scss index 80694347393ce..026c23b93b040 100644 --- a/src/core/public/chrome/ui/_loading_indicator.scss +++ b/src/core/public/chrome/ui/_loading_indicator.scss @@ -22,29 +22,34 @@ $kbnLoadingIndicatorColor2: tint($euiColorAccent, 60%); } } - .kbnLoadingIndicator__bar { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - z-index: $euiZLevel1 + 1; - visibility: visible; - display: block; - animation: kbn-animate-loading-indicator 2s linear infinite; - background-color: $kbnLoadingIndicatorColor2; - background-image: linear-gradient(to right, - $kbnLoadingIndicatorColor1 0%, - $kbnLoadingIndicatorColor1 50%, - $kbnLoadingIndicatorColor2 50%, - $kbnLoadingIndicatorColor2 100% - ); - background-repeat: repeat-x; - background-size: $kbnLoadingIndicatorBackgroundSize $kbnLoadingIndicatorBackgroundSize; - width: 200%; - } +.kbnLoadingIndicator__bar { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + z-index: $euiZLevel1 + 1; + visibility: visible; + display: block; + animation: kbn-animate-loading-indicator 2s linear infinite; + background-color: $kbnLoadingIndicatorColor2; + background-image: linear-gradient( + to right, + $kbnLoadingIndicatorColor1 0%, + $kbnLoadingIndicatorColor1 50%, + $kbnLoadingIndicatorColor2 50%, + $kbnLoadingIndicatorColor2 100% + ); + background-repeat: repeat-x; + background-size: $kbnLoadingIndicatorBackgroundSize $kbnLoadingIndicatorBackgroundSize; + width: 200%; +} - @keyframes kbn-animate-loading-indicator { - from { transform: translateX(0); } - to { transform: translateX(-$kbnLoadingIndicatorBackgroundSize); } +@keyframes kbn-animate-loading-indicator { + from { + transform: translateX(0); } + to { + transform: translateX(-$kbnLoadingIndicatorBackgroundSize); + } +} diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index c9a583f39b30c..4dec084fd8a83 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -30,6 +30,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Component, createRef } from 'react'; +import classnames from 'classnames'; import * as Rx from 'rxjs'; import { ChromeBadge, @@ -68,8 +69,8 @@ export interface HeaderProps { navControlsLeft$: Rx.Observable; navControlsRight$: Rx.Observable; basePath: HttpStart['basePath']; - isLocked?: boolean; - onIsLockedUpdate?: OnIsLockedUpdate; + isLocked$: Rx.Observable; + onIsLockedUpdate: OnIsLockedUpdate; } interface State { @@ -81,6 +82,7 @@ interface State { navControlsLeft: readonly ChromeNavControl[]; navControlsRight: readonly ChromeNavControl[]; currentAppId: string | undefined; + isLocked: boolean; } export class Header extends Component { @@ -99,6 +101,7 @@ export class Header extends Component { navControlsLeft: [], navControlsRight: [], currentAppId: '', + isLocked: false, }; } @@ -109,11 +112,12 @@ export class Header extends Component { this.props.forceAppSwitcherNavigation$, this.props.navLinks$, this.props.recentlyAccessed$, - // Types for combineLatest only handle up to 6 inferred types so we combine these two separately. + // Types for combineLatest only handle up to 6 inferred types so we combine these separately. Rx.combineLatest( this.props.navControlsLeft$, this.props.navControlsRight$, - this.props.application.currentAppId$ + this.props.application.currentAppId$, + this.props.isLocked$ ) ).subscribe({ next: ([ @@ -122,7 +126,7 @@ export class Header extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId], + [navControlsLeft, navControlsRight, currentAppId, isLocked], ]) => { this.setState({ appTitle, @@ -133,6 +137,7 @@ export class Header extends Component { navControlsLeft, navControlsRight, currentAppId, + isLocked, }); }, }); @@ -181,8 +186,16 @@ export class Header extends Component { return null; } + const className = classnames( + 'chrHeaderWrapper', + { + 'chrHeaderWrapper--navIsLocked': this.state.isLocked, + }, + 'hide-for-sharing' + ); + return ( -

+
@@ -220,7 +233,7 @@ export class Header extends Component { = props => { - const initialIsLocked = localStorage.getItem(IS_LOCKED_KEY); - const [isLocked, setIsLocked] = useState(initialIsLocked === 'true'); - const setIsLockedStored = (locked: boolean) => { - localStorage.setItem(IS_LOCKED_KEY, `${locked}`); - setIsLocked(locked); - }; - const className = classnames( - 'chrHeaderWrapper', - { - 'chrHeaderWrapper--navIsLocked': isLocked, - }, - 'hide-for-sharing' - ); - return ( -
-
-
- ); -}; diff --git a/src/core/public/chrome/ui/header/index.ts b/src/core/public/chrome/ui/header/index.ts index 4521f1f74b31b..49e002a66d939 100644 --- a/src/core/public/chrome/ui/header/index.ts +++ b/src/core/public/chrome/ui/header/index.ts @@ -18,7 +18,6 @@ */ export { Header, HeaderProps } from './header'; -export { HeaderWrapper } from './header_wrapper'; export { ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, diff --git a/src/core/public/chrome/ui/index.ts b/src/core/public/chrome/ui/index.ts index 81b2fdfb0fcc0..460e19b7d9780 100644 --- a/src/core/public/chrome/ui/index.ts +++ b/src/core/public/chrome/ui/index.ts @@ -20,7 +20,6 @@ export { LoadingIndicator } from './loading_indicator'; export { Header, - HeaderWrapper, ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, ChromeHelpExtensionMenuDiscussLink, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index fa5dc745e6931..7428280b2dccb 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -337,7 +337,7 @@ export interface ChromeStart { getBrand$(): Observable; getBreadcrumbs$(): Observable; getHelpExtension$(): Observable; - getIsCollapsed$(): Observable; + getIsNavDrawerLocked$(): Observable; getIsVisible$(): Observable; navControls: ChromeNavControls; navLinks: ChromeNavLinks; @@ -349,7 +349,6 @@ export interface ChromeStart { setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; setHelpExtension(helpExtension?: ChromeHelpExtension): void; setHelpSupportUrl(url: string): void; - setIsCollapsed(isCollapsed: boolean): void; setIsVisible(isVisible: boolean): void; } diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index bb0a8616e7222..9789d266587af 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -179,7 +179,7 @@ export interface RouteConfig { * access to raw values. * In some cases you may want to use another validation library. To do this, you need to * instruct the `@kbn/config-schema` library to output **non-validated values** with - * setting schema as `schema.object({}, { allowUnknowns: true })`; + * setting schema as `schema.object({}, { unknowns: 'allow' })`; * * @example * ```ts @@ -212,7 +212,7 @@ export interface RouteConfig { * path: 'path/{id}', * validate: { * // handler has access to raw non-validated params in runtime - * params: schema.object({}, { allowUnknowns: true }) + * params: schema.object({}, { unknowns: 'allow' }) * }, * }, * (context, req, res,) { diff --git a/src/core/server/http/router/router.test.ts b/src/core/server/http/router/router.test.ts index a936da6a40a9f..9655e2153b863 100644 --- a/src/core/server/http/router/router.test.ts +++ b/src/core/server/http/router/router.test.ts @@ -59,7 +59,7 @@ describe('Router', () => { { path: '/', options: { body: { output: 'file' } } as any, // We explicitly don't support 'file' - validate: { body: schema.object({}, { allowUnknowns: true }) }, + validate: { body: schema.object({}, { unknowns: 'allow' }) }, }, (context, req, res) => res.ok({}) ) diff --git a/src/core/server/rendering/views/styles.tsx b/src/core/server/rendering/views/styles.tsx index 9ab9f2ad0d6b8..71b42e3464118 100644 --- a/src/core/server/rendering/views/styles.tsx +++ b/src/core/server/rendering/views/styles.tsx @@ -53,7 +53,7 @@ export const Styles: FunctionComponent = ({ darkMode }) => { .kbnWelcomeView { line-height: 1.5; - background-color: #FFF; + background-color: ${darkMode ? '#1D1E24' : '#FFF'}; height: 100%; display: -webkit-box; display: -webkit-flex; @@ -97,6 +97,7 @@ export const Styles: FunctionComponent = ({ darkMode }) => { line-height: 40px !important; height: 40px !important; color: #98a2b3; + color: ${darkMode ? '#98A2B3' : '#69707D'}; } .kbnLoaderWrap { @@ -128,7 +129,7 @@ export const Styles: FunctionComponent = ({ darkMode }) => { width: 32px; height: 4px; overflow: hidden; - background-color: #D3DAE6; + background-color: ${darkMode ? '#25262E' : '#F5F7FA'}; line-height: 1; } @@ -142,7 +143,7 @@ export const Styles: FunctionComponent = ({ darkMode }) => { left: 0; transform: scaleX(0) translateX(0%); animation: kbnProgress 1s cubic-bezier(.694, .0482, .335, 1) infinite; - background-color: #006DE4; + background-color: ${darkMode ? '#1BA9F5' : '#006DE4'}; } @keyframes kbnProgress { diff --git a/src/core/server/ui_settings/routes/set_many.ts b/src/core/server/ui_settings/routes/set_many.ts index 5623c3fe11b80..d19a36a7ce768 100644 --- a/src/core/server/ui_settings/routes/set_many.ts +++ b/src/core/server/ui_settings/routes/set_many.ts @@ -24,7 +24,7 @@ import { CannotOverrideError } from '../ui_settings_errors'; const validate = { body: schema.object({ - changes: schema.object({}, { allowUnknowns: true }), + changes: schema.object({}, { unknowns: 'allow' }), }), }; diff --git a/src/core/server/ui_settings/ui_settings_config.ts b/src/core/server/ui_settings/ui_settings_config.ts index a54d482a0296a..a0ac48e2dd089 100644 --- a/src/core/server/ui_settings/ui_settings_config.ts +++ b/src/core/server/ui_settings/ui_settings_config.ts @@ -39,7 +39,7 @@ const configSchema = schema.object({ }) ), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), }); diff --git a/src/legacy/core_plugins/kibana/common/utils/__tests__/shorten_dotted_string.js b/src/legacy/core_plugins/kibana/common/utils/__tests__/shorten_dotted_string.js deleted file mode 100644 index 267ca74c7c42a..0000000000000 --- a/src/legacy/core_plugins/kibana/common/utils/__tests__/shorten_dotted_string.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import { shortenDottedString } from '../shorten_dotted_string'; - -describe('shortenDottedString', () => { - it('Convert a dot.notated.string into a short string', () => { - expect(shortenDottedString('dot.notated.string')).to.equal('d.n.string'); - }); - - it('Ignores non-string values', () => { - expect(shortenDottedString(true)).to.equal(true); - expect(shortenDottedString(123)).to.equal(123); - const obj = { key: 'val' }; - expect(shortenDottedString(obj)).to.equal(obj); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index 0c5329d8b259f..3f81bfe5aadf2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -28,12 +28,11 @@ export { npSetup, npStart } from 'ui/new_platform'; export { KbnUrl } from 'ui/url/kbn_url'; // @ts-ignore -export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url/index'; +export { KbnUrlProvider } from 'ui/url/index'; export { IInjector } from 'ui/chrome'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { configureAppAngularModule, - ensureDefaultIndexPattern, IPrivate, migrateLegacyQuery, PrivateProvider, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index fe0e7a1d3e6d0..9447b5384d172 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -35,7 +35,6 @@ import { KbnUrlProvider, PrivateProvider, PromiseServiceCreator, - RedirectWhenMissingProvider, } from '../legacy_imports'; // @ts-ignore import { initDashboardApp } from './legacy_app'; @@ -146,8 +145,7 @@ function createLocalIconModule() { function createLocalKbnUrlModule() { angular .module('app/dashboard/KbnUrl', ['app/dashboard/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalConfigModule(core: AppMountContext['core']) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index 35b510894179d..64abbdfb87d58 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -23,11 +23,12 @@ import dashboardTemplate from './dashboard_app.html'; import dashboardListingTemplate from './listing/dashboard_listing_ng_wrapper.html'; import { createHashHistory } from 'history'; -import { ensureDefaultIndexPattern } from '../legacy_imports'; import { initDashboardAppDirective } from './dashboard_app'; import { createDashboardEditUrl, DashboardConstants } from './dashboard_constants'; import { createKbnUrlStateStorage, + ensureDefaultIndexPattern, + redirectWhenMissing, InvalidJSONProperty, SavedObjectNotFound, } from '../../../../../../plugins/kibana_utils/public'; @@ -136,8 +137,8 @@ export function initDashboardApp(app, deps) { }); }, resolve: { - dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { - return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl).then(() => { + dash: function($route, history) { + return ensureDefaultIndexPattern(deps.core, deps.data, history).then(() => { const savedObjectsClient = deps.savedObjectsClient; const title = $route.current.params.title; if (title) { @@ -171,17 +172,18 @@ export function initDashboardApp(app, deps) { controller: createNewDashboardCtrl, requireUICapability: 'dashboard.createNew', resolve: { - dash: function(redirectWhenMissing, $rootScope, kbnUrl) { - return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) - .then(() => { - return deps.savedDashboards.get(); - }) + dash: history => + ensureDefaultIndexPattern(deps.core, deps.data, history) + .then(() => deps.savedDashboards.get()) .catch( redirectWhenMissing({ - dashboard: DashboardConstants.LANDING_PAGE_PATH, + history, + mapping: { + dashboard: DashboardConstants.LANDING_PAGE_PATH, + }, + toastNotifications: deps.core.notifications.toasts, }) - ); - }, + ), }, }) .when(createDashboardEditUrl(':id'), { @@ -189,13 +191,11 @@ export function initDashboardApp(app, deps) { template: dashboardTemplate, controller: createNewDashboardCtrl, resolve: { - dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { + dash: function($route, kbnUrl, history) { const id = $route.current.params.id; - return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) - .then(() => { - return deps.savedDashboards.get(id); - }) + return ensureDefaultIndexPattern(deps.core, deps.data, history) + .then(() => deps.savedDashboards.get(id)) .then(savedDashboard => { deps.chrome.recentlyAccessed.add( savedDashboard.getFullPath(), @@ -207,7 +207,7 @@ export function initDashboardApp(app, deps) { .catch(error => { // A corrupt dashboard was detected (e.g. with invalid JSON properties) if (error instanceof InvalidJSONProperty) { - deps.toastNotifications.addDanger(error.message); + deps.core.notifications.toasts.addDanger(error.message); kbnUrl.redirect(DashboardConstants.LANDING_PAGE_PATH); return; } @@ -221,7 +221,7 @@ export function initDashboardApp(app, deps) { pathname: DashboardConstants.CREATE_NEW_DASHBOARD_URL, }); - deps.toastNotifications.addWarning( + deps.core.notifications.toasts.addWarning( i18n.translate('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', { defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', @@ -234,7 +234,11 @@ export function initDashboardApp(app, deps) { }) .catch( redirectWhenMissing({ - dashboard: DashboardConstants.LANDING_PAGE_PATH, + history, + mapping: { + dashboard: DashboardConstants.LANDING_PAGE_PATH, + }, + toastNotifications: deps.core.notifications.toasts, }) ); }, diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index c58307adaf38c..282eef0c983eb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { createHashHistory, History } from 'history'; + import { Capabilities, ChromeStart, @@ -46,6 +48,7 @@ export interface DiscoverServices { data: DataPublicPluginStart; docLinks: DocLinksStart; docViewsRegistry: DocViewsRegistry; + history: History; theme: ChartsPluginStart['theme']; filterManager: FilterManager; indexPatterns: IndexPatternsContract; @@ -79,6 +82,7 @@ export async function buildServices( data: plugins.data, docLinks: core.docLinks, docViewsRegistry, + history: createHashHistory(), theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, getSavedSearchById: async (id: string) => savedObjectService.get(id), diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 76d475c4f7f96..4d871bcb7a858 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -27,7 +27,7 @@ import { CoreStart, LegacyCoreStart, IUiSettingsClient } from 'kibana/public'; // @ts-ignore import { StateManagementConfigProvider } from 'ui/state_management/config_provider'; // @ts-ignore -import { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; +import { KbnUrlProvider } from 'ui/url'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -173,8 +173,7 @@ export function initializeInnerAngularModule( function createLocalKbnUrlModule() { angular .module('discoverKbnUrl', ['discoverPrivate', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalConfigModule(uiSettings: IUiSettingsClient) { diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 57a9e4966d6d6..725e94f16e2e8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -53,17 +53,18 @@ export { wrapInI18nContext } from 'ui/i18n'; import { search } from '../../../../../plugins/data/public'; export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; // @ts-ignore -export { shortenDottedString } from '../../common/utils/shorten_dotted_string'; -// @ts-ignore export { intervalOptions } from 'ui/agg_types'; -export { subscribeWithScope } from '../../../../../plugins/kibana_legacy/public'; // @ts-ignore export { timezoneProvider } from 'ui/vis/lib/timezone'; -export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; export { + unhashUrl, + redirectWhenMissing, ensureDefaultIndexPattern, +} from '../../../../../plugins/kibana_utils/public'; +export { formatMsg, formatStack, + subscribeWithScope, } from '../../../../../plugins/kibana_legacy/public'; // EXPORT types diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx index 26d8a5abb2471..1b3b16332fa4f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx @@ -21,7 +21,7 @@ import classNames from 'classnames'; import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { FieldIcon, FieldIconProps } from '../../../../../../../../../plugins/kibana_react/public'; -import { shortenDottedString } from '../../../../kibana_services'; +import { shortenDottedString } from '../../../helpers'; import { getFieldTypeName } from './field_type_name'; // property field is provided at discover's field chooser diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index f3334c9211e4b..9a383565f4f43 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -50,6 +50,7 @@ import { tabifyAggResponse, getAngularModule, ensureDefaultIndexPattern, + redirectWhenMissing, } from '../../kibana_services'; const { @@ -57,6 +58,7 @@ const { chrome, data, docTitle, + history, indexPatterns, filterManager, share, @@ -113,10 +115,10 @@ app.config($routeProvider => { template: indexTemplate, reloadOnSearch: false, resolve: { - savedObjects: function(redirectWhenMissing, $route, kbnUrl, Promise, $rootScope) { + savedObjects: function($route, Promise) { const savedSearchId = $route.current.params.id; - return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl).then(() => { - const { appStateContainer } = getState({}); + return ensureDefaultIndexPattern(core, data, history).then(() => { + const { appStateContainer } = getState({ history }); const { index } = appStateContainer.getState(); return Promise.props({ ip: indexPatterns.getCache().then(indexPatternList => { @@ -151,9 +153,13 @@ app.config($routeProvider => { }) .catch( redirectWhenMissing({ - search: '/discover', - 'index-pattern': - '/management/kibana/objects/savedSearches/' + $route.current.params.id, + history, + mapping: { + search: '/discover', + 'index-pattern': + '/management/kibana/objects/savedSearches/' + $route.current.params.id, + }, + toastNotifications, }) ), }); @@ -207,6 +213,7 @@ function discoverController( } = getState({ defaultAppState: getStateDefaults(), storeInSessionStorage: config.get('state:storeInSessionStorage'), + history, }); if (appStateContainer.getState().index !== $scope.indexPattern.id) { //used index pattern is different than the given by url/state which is invalid diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts index af772cb5c76f1..3840fd0c2e3be 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts @@ -30,7 +30,7 @@ describe('Test discover state', () => { history.push('/'); state = getState({ defaultAppState: { index: 'test' }, - hashHistory: history, + history, }); await state.replaceUrlAppState({}); await state.startSync(); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts index 10e7cd1d0c49d..981855d1ee774 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts @@ -17,7 +17,7 @@ * under the License. */ import { isEqual } from 'lodash'; -import { createHashHistory, History } from 'history'; +import { History } from 'history'; import { createStateContainer, createKbnUrlStateStorage, @@ -65,9 +65,9 @@ interface GetStateParams { */ storeInSessionStorage?: boolean; /** - * Browser history used for testing + * Browser history */ - hashHistory?: History; + history: History; } export interface GetStateReturn { @@ -121,11 +121,11 @@ const APP_STATE_URL_KEY = '_a'; export function getState({ defaultAppState = {}, storeInSessionStorage = false, - hashHistory, + history, }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, - history: hashHistory ? hashHistory : createHashHistory(), + history, }); const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx index a2ad18d59d935..bd48b1e083871 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { IndexPattern, shortenDottedString } from '../../../../../kibana_services'; +import { IndexPattern } from '../../../../../kibana_services'; +import { shortenDottedString } from '../../../../helpers'; export type SortOrder = [string, string]; export interface ColumnProps { diff --git a/src/legacy/ui/public/chrome/services/index.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/index.ts similarity index 92% rename from src/legacy/ui/public/chrome/services/index.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/index.ts index 3b3967f51b2ff..7196c96989e97 100644 --- a/src/legacy/ui/public/chrome/services/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/index.ts @@ -17,4 +17,4 @@ * under the License. */ -import './global_nav_state'; +export { shortenDottedString } from './shorten_dotted_string'; diff --git a/src/legacy/core_plugins/kibana/common/utils/shorten_dotted_string.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/shorten_dotted_string.ts similarity index 81% rename from src/legacy/core_plugins/kibana/common/utils/shorten_dotted_string.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/shorten_dotted_string.ts index ca76a2a537742..9d78a96784339 100644 --- a/src/legacy/core_plugins/kibana/common/utils/shorten_dotted_string.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/shorten_dotted_string.ts @@ -22,10 +22,5 @@ const DOT_PREFIX_RE = /(.).+?\./g; /** * Convert a dot.notated.string into a short * version (d.n.string) - * - * @param {string} str - the long string to convert - * @return {string} */ -export function shortenDottedString(input) { - return typeof input !== 'string' ? input : input.replace(DOT_PREFIX_RE, '$1.'); -} +export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.'); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap index 805131042f385..a4dcfb9c38184 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap @@ -126,6 +126,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` Array [ Object { "align": "center", + "data-test-subj": "savedObjectsTableRowType", "description": "Type of the saved object", "field": "type", "name": "Type", @@ -134,6 +135,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` "width": "50px", }, Object { + "data-test-subj": "savedObjectsTableRowTitle", "dataType": "string", "description": "Title of the saved object", "field": "meta.title", @@ -145,6 +147,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` "actions": Array [ Object { "available": [Function], + "data-test-subj": "savedObjectsTableAction-inspect", "description": "Inspect this saved object", "icon": "inspect", "name": "Inspect", @@ -152,6 +155,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` "type": "icon", }, Object { + "data-test-subj": "savedObjectsTableAction-relationships", "description": "View the relationships this saved object has to other saved objects", "icon": "kqlSelector", "name": "Relationships", @@ -198,6 +202,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` } } responsive={true} + rowProps={[Function]} selection={ Object { "onSelectionChange": [Function], @@ -334,6 +339,7 @@ exports[`Table should render normally 1`] = ` Array [ Object { "align": "center", + "data-test-subj": "savedObjectsTableRowType", "description": "Type of the saved object", "field": "type", "name": "Type", @@ -342,6 +348,7 @@ exports[`Table should render normally 1`] = ` "width": "50px", }, Object { + "data-test-subj": "savedObjectsTableRowTitle", "dataType": "string", "description": "Title of the saved object", "field": "meta.title", @@ -353,6 +360,7 @@ exports[`Table should render normally 1`] = ` "actions": Array [ Object { "available": [Function], + "data-test-subj": "savedObjectsTableAction-inspect", "description": "Inspect this saved object", "icon": "inspect", "name": "Inspect", @@ -360,6 +368,7 @@ exports[`Table should render normally 1`] = ` "type": "icon", }, Object { + "data-test-subj": "savedObjectsTableAction-relationships", "description": "View the relationships this saved object has to other saved objects", "icon": "kqlSelector", "name": "Relationships", @@ -406,6 +415,7 @@ exports[`Table should render normally 1`] = ` } } responsive={true} + rowProps={[Function]} selection={ Object { "onSelectionChange": [Function], diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js index a119817fdc0c9..386b35399b754 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js @@ -178,6 +178,7 @@ export class Table extends PureComponent { { defaultMessage: 'Type of the saved object' } ), sortable: false, + 'data-test-subj': 'savedObjectsTableRowType', render: (type, object) => { return ( @@ -201,6 +202,7 @@ export class Table extends PureComponent { ), dataType: 'string', sortable: false, + 'data-test-subj': 'savedObjectsTableRowTitle', render: (title, object) => { const { path } = object.meta.inAppUrl || {}; const canGoInApp = this.props.canGoInApp(object); @@ -230,6 +232,7 @@ export class Table extends PureComponent { icon: 'inspect', onClick: object => goInspectObject(object), available: object => !!object.meta.editUrl, + 'data-test-subj': 'savedObjectsTableAction-inspect', }, { name: i18n.translate( @@ -246,10 +249,12 @@ export class Table extends PureComponent { type: 'icon', icon: 'kqlSelector', onClick: object => onShowRelationships(object), + 'data-test-subj': 'savedObjectsTableAction-relationships', }, ...this.extraActions.map(action => { return { ...action.euiAction, + 'data-test-subj': `savedObjectsTableAction-${action.id}`, onClick: object => { this.setState({ activeAction: action, @@ -372,6 +377,9 @@ export class Table extends PureComponent { pagination={pagination} selection={selection} onChange={onTableChange} + rowProps={item => ({ + 'data-test-subj': `savedObjectsTableRow row-${item.id}`, + })} /> diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 0ddf3ee67aa94..e6b7a29e28d89 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -25,7 +25,7 @@ */ // @ts-ignore -export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; +export { KbnUrlProvider } from 'ui/url'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; export { wrapInI18nContext } from 'ui/i18n'; @@ -33,7 +33,6 @@ export { DashboardConstants } from '../dashboard/np_ready/dashboard_constants'; export { VisSavedObject, VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/'; export { configureAppAngularModule, - ensureDefaultIndexPattern, IPrivate, migrateLegacyQuery, PrivateProvider, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index 8ef63ec5778e2..c7c3286bb5c71 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -24,7 +24,6 @@ import { AppMountContext } from 'kibana/public'; import { configureAppAngularModule, KbnUrlProvider, - RedirectWhenMissingProvider, IPrivate, PrivateProvider, PromiseServiceCreator, @@ -102,8 +101,7 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav function createLocalKbnUrlModule() { angular .module('app/visualize/KbnUrl', ['app/visualize/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalPromiseModule() { diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index c023c402f5fea..1fab38027f65b 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -31,6 +31,7 @@ import { getEditBreadcrumbs } from '../breadcrumbs'; import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; import { unhashUrl } from '../../../../../../../plugins/kibana_utils/public'; +import { MarkdownSimple, toMountPoint } from '../../../../../../../plugins/kibana_react/public'; import { addFatalError, kbnBaseUrl } from '../../../../../../../plugins/kibana_legacy/public'; import { SavedObjectSaveModal, @@ -75,7 +76,6 @@ function VisualizeAppController( $injector, $timeout, kbnUrl, - redirectWhenMissing, kbnUrlStateStorage, history ) { @@ -313,16 +313,33 @@ function VisualizeAppController( } ); + const stopAllSyncing = () => { + stopStateSync(); + stopSyncingQueryServiceStateWithUrl(); + stopSyncingAppFilters(); + }; + // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the // defaults applied. If the url was from a previous session which included modifications to the // appState then they won't be equal. if (!_.isEqual(stateContainer.getState().vis, stateDefaults.vis)) { try { vis.setState(stateContainer.getState().vis); - } catch { - redirectWhenMissing({ - 'index-pattern-field': '/visualize', + } catch (error) { + // stop syncing url updtes with the state to prevent extra syncing + stopAllSyncing(); + + toastNotifications.addWarning({ + title: i18n.translate('kbn.visualize.visualizationTypeInvalidNotificationMessage', { + defaultMessage: 'Invalid visualization type', + }), + text: toMountPoint({error.message}), }); + + history.replace(`${VisualizeConstants.LANDING_PAGE_PATH}?notFound=visualization`); + + // prevent further controller execution + return; } } @@ -529,9 +546,8 @@ function VisualizeAppController( unsubscribePersisted(); unsubscribeStateUpdates(); - stopStateSync(); - stopSyncingQueryServiceStateWithUrl(); - stopSyncingAppFilters(); + + stopAllSyncing(); }); $timeout(() => { diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js index 6acdb0abdd0b5..c8acea168444f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js @@ -59,7 +59,9 @@ export function initVisualizationDirective(app, deps) { }); $scope.$on('$destroy', () => { - $scope._handler.destroy(); + if ($scope._handler) { + $scope._handler.destroy(); + } }); }, }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index b9409445166bc..0f1d50b149cd9 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -21,7 +21,11 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { createHashHistory } from 'history'; -import { createKbnUrlStateStorage } from '../../../../../../plugins/kibana_utils/public'; +import { + createKbnUrlStateStorage, + redirectWhenMissing, + ensureDefaultIndexPattern, +} from '../../../../../../plugins/kibana_utils/public'; import editorTemplate from './editor/editor.html'; import visualizeListingTemplate from './listing/visualize_listing.html'; @@ -29,7 +33,6 @@ import visualizeListingTemplate from './listing/visualize_listing.html'; import { initVisualizeAppDirective } from './visualize_app'; import { VisualizeConstants } from './visualize_constants'; import { VisualizeListingController } from './listing/visualize_listing'; -import { ensureDefaultIndexPattern } from '../legacy_imports'; import { getLandingBreadcrumbs, @@ -79,8 +82,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => false, - hasDefaultIndex: ($rootScope, kbnUrl) => - ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl), + hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), }, }) .when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, { @@ -91,8 +93,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => true, - hasDefaultIndex: ($rootScope, kbnUrl) => - ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl), + hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), }, }) .when(VisualizeConstants.CREATE_PATH, { @@ -100,8 +101,8 @@ export function initVisualizeApp(app, deps) { template: editorTemplate, k7Breadcrumbs: getCreateBreadcrumbs, resolve: { - savedVis: function(redirectWhenMissing, $route, $rootScope, kbnUrl) { - const { core, data, savedVisualizations, visualizations } = deps; + savedVis: function($route, history) { + const { core, data, savedVisualizations, visualizations, toastNotifications } = deps; const visTypes = visualizations.all(); const visType = find(visTypes, { name: $route.current.params.type }); const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection; @@ -118,7 +119,7 @@ export function initVisualizeApp(app, deps) { ); } - return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl) + return ensureDefaultIndexPattern(core, data, history) .then(() => savedVisualizations.get($route.current.params)) .then(savedVis => { if (savedVis.vis.type.setup) { @@ -128,7 +129,9 @@ export function initVisualizeApp(app, deps) { }) .catch( redirectWhenMissing({ - '*': '/visualize', + history, + mapping: VisualizeConstants.LANDING_PAGE_PATH, + toastNotifications, }) ); }, @@ -139,9 +142,9 @@ export function initVisualizeApp(app, deps) { template: editorTemplate, k7Breadcrumbs: getEditBreadcrumbs, resolve: { - savedVis: function(redirectWhenMissing, $route, $rootScope, kbnUrl) { - const { chrome, core, data, savedVisualizations } = deps; - return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl) + savedVis: function($route, history) { + const { chrome, core, data, savedVisualizations, toastNotifications } = deps; + return ensureDefaultIndexPattern(core, data, history) .then(() => savedVisualizations.get($route.current.params.id)) .then(savedVis => { chrome.recentlyAccessed.add(savedVis.getFullPath(), savedVis.title, savedVis.id); @@ -155,13 +158,17 @@ export function initVisualizeApp(app, deps) { }) .catch( redirectWhenMissing({ - visualization: '/visualize', - search: - '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, - 'index-pattern': - '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, - 'index-pattern-field': - '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + history, + mapping: { + visualization: VisualizeConstants.LANDING_PAGE_PATH, + search: + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + 'index-pattern': + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + 'index-pattern-field': + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + }, + toastNotifications, }) ); }, diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index 0263f5b2c851c..ff2546f75c51a 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -50,7 +50,7 @@ export class VisEditor extends Component { visFields: props.visFields, extractedIndexPatterns: [''], }; - this.onBrush = createBrushHandler(getDataStart().query.timefilter); + this.onBrush = createBrushHandler(getDataStart().query.timefilter.timefilter); this.visDataSubject = new Rx.BehaviorSubject(this.props.visData); this.visData$ = this.visDataSubject.asObservable().pipe(share()); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss index 90c2007b1c94a..3db09bace079f 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss @@ -7,4 +7,21 @@ .tvbVisTimeSeries { overflow: hidden; } + .tvbVisTimeSeriesDark { + .echReactiveChart_unavailable { + color: #DFE5EF; + } + .echLegendItem { + color: #DFE5EF; + } + } + .tvbVisTimeSeriesLight { + .echReactiveChart_unavailable { + color: #343741; + } + .echLegendItem { + color: #343741; + } + } } + diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js index 954d3d174bb8c..356ba08ac2427 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js @@ -33,9 +33,8 @@ import { getAxisLabelString } from '../../lib/get_axis_label_string'; import { getInterval } from '../../lib/get_interval'; import { areFieldsDifferent } from '../../lib/charts'; import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; -import { isBackgroundDark } from '../../../lib/set_is_reversed'; import { STACKED_OPTIONS } from '../../../visualizations/constants'; -import { getCoreStart } from '../../../services'; +import { getCoreStart, getUISettings } from '../../../services'; export class TimeseriesVisualization extends Component { static propTypes = { @@ -238,6 +237,7 @@ export class TimeseriesVisualization extends Component { } }); + const darkMode = getUISettings().get('theme:darkMode'); return (
null; export const AreaSeries = () => null; + +export { LIGHT_THEME, DARK_THEME } from '@elastic/charts'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js index 986111b462b35..75554a476bdea 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js @@ -19,14 +19,13 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { Axis, Chart, Position, Settings, - DARK_THEME, - LIGHT_THEME, AnnotationDomainTypes, LineAnnotation, TooltipType, @@ -40,6 +39,7 @@ import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constan import { AreaSeriesDecorator } from './decorators/area_decorator'; import { BarSeriesDecorator } from './decorators/bar_decorator'; import { getStackAccessors } from './utils/stack_format'; +import { getTheme, getChartClasses } from './utils/theme'; const generateAnnotationData = (values, formatter) => values.map(({ key, docs }) => ({ @@ -57,7 +57,8 @@ const handleCursorUpdate = cursor => { }; export const TimeSeries = ({ - isDarkMode, + darkMode, + backgroundColor, showGrid, legend, legendPosition, @@ -89,8 +90,13 @@ export const TimeSeries = ({ const timeZone = timezoneProvider(uiSettings)(); const hasBarChart = series.some(({ bars }) => bars.show); + // compute the theme based on the bg color + const theme = getTheme(darkMode, backgroundColor); + // apply legend style change if bgColor is configured + const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor)); + return ( - + { + it('should return the basic themes if no bg color is specified', () => { + // use original dark/light theme + expect(getTheme(false)).toEqual(LIGHT_THEME); + expect(getTheme(true)).toEqual(DARK_THEME); + + // discard any wrong/missing bg color + expect(getTheme(true, null)).toEqual(DARK_THEME); + expect(getTheme(true, '')).toEqual(DARK_THEME); + expect(getTheme(true, undefined)).toEqual(DARK_THEME); + }); + it('should return a highcontrast color theme for a different background', () => { + // red use a near full-black color + expect(getTheme(false, 'red').axes.axisTitleStyle.fill).toEqual('rgb(23,23,23)'); + + // violet increased the text color to full white for higer contrast + expect(getTheme(false, '#ba26ff').axes.axisTitleStyle.fill).toEqual('rgb(255,255,255)'); + + // light yellow, prefer the LIGHT_THEME fill color because already with a good contrast + expect(getTheme(false, '#fff49f').axes.axisTitleStyle.fill).toEqual('#333'); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts new file mode 100644 index 0000000000000..a25d5e1ce1d35 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts @@ -0,0 +1,139 @@ +/* + * 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 colorJS from 'color'; +import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts'; + +function computeRelativeLuminosity(rgb: string) { + return colorJS(rgb).luminosity(); +} + +function computeContrast(rgb1: string, rgb2: string) { + return colorJS(rgb1).contrast(colorJS(rgb2)); +} + +function getAAARelativeLum(bgColor: string, fgColor: string, ratio = 7) { + const relLum1 = computeRelativeLuminosity(bgColor); + const relLum2 = computeRelativeLuminosity(fgColor); + if (relLum1 > relLum2) { + // relLum1 is brighter, relLum2 is darker + return (relLum1 + 0.05 - ratio * 0.05) / ratio; + } else { + // relLum1 is darker, relLum2 is brighter + return Math.min(ratio * (relLum1 + 0.05) - 0.05, 1); + } +} + +function getGrayFromRelLum(relLum: number) { + if (relLum <= 0.0031308) { + return relLum * 12.92; + } else { + return (1.0 + 0.055) * Math.pow(relLum, 1.0 / 2.4) - 0.055; + } +} + +function getGrayRGBfromGray(gray: number) { + const g = Math.round(gray * 255); + return `rgb(${g},${g},${g})`; +} + +function getAAAGray(bgColor: string, fgColor: string, ratio = 7) { + const relLum = getAAARelativeLum(bgColor, fgColor, ratio); + const gray = getGrayFromRelLum(relLum); + return getGrayRGBfromGray(gray); +} + +function findBestContrastColor( + bgColor: string, + lightFgColor: string, + darkFgColor: string, + ratio = 4.5 +) { + const lc = computeContrast(bgColor, lightFgColor); + const dc = computeContrast(bgColor, darkFgColor); + if (lc >= dc) { + if (lc >= ratio) { + return lightFgColor; + } + return getAAAGray(bgColor, lightFgColor, ratio); + } + if (dc >= ratio) { + return darkFgColor; + } + return getAAAGray(bgColor, darkFgColor, ratio); +} + +function isValidColor(color: string | null | undefined): color is string { + if (typeof color !== 'string') { + return false; + } + if (color.length === 0) { + return false; + } + try { + colorJS(color); + return true; + } catch { + return false; + } +} + +export function getTheme(darkMode: boolean, bgColor?: string | null): Theme { + if (!isValidColor(bgColor)) { + return darkMode ? DARK_THEME : LIGHT_THEME; + } + + const bgLuminosity = computeRelativeLuminosity(bgColor); + const mainTheme = bgLuminosity <= 0.179 ? DARK_THEME : LIGHT_THEME; + const color = findBestContrastColor( + bgColor, + LIGHT_THEME.axes.axisTitleStyle.fill, + DARK_THEME.axes.axisTitleStyle.fill + ); + return { + ...mainTheme, + axes: { + ...mainTheme.axes, + axisTitleStyle: { + ...mainTheme.axes.axisTitleStyle, + fill: color, + }, + tickLabelStyle: { + ...mainTheme.axes.tickLabelStyle, + fill: color, + }, + axisLineStyle: { + ...mainTheme.axes.axisLineStyle, + stroke: color, + }, + tickLineStyle: { + ...mainTheme.axes.tickLineStyle, + stroke: color, + }, + }, + }; +} + +export function getChartClasses(bgColor?: string) { + // keep the original theme color if no bg color is specified + if (typeof bgColor !== 'string') { + return; + } + const bgLuminosity = computeRelativeLuminosity(bgColor); + return bgLuminosity <= 0.179 ? 'tvbVisTimeSeriesDark' : 'tvbVisTimeSeriesLight'; +} diff --git a/src/legacy/ui/public/chrome/chrome.js b/src/legacy/ui/public/chrome/chrome.js index 3355870eabfe7..7a75ad906a870 100644 --- a/src/legacy/ui/public/chrome/chrome.js +++ b/src/legacy/ui/public/chrome/chrome.js @@ -28,7 +28,6 @@ import '../private'; import '../promises'; import '../directives/storage'; import '../directives/watch_multi'; -import './services'; import '../react_components'; import '../i18n'; diff --git a/src/legacy/ui/public/chrome/services/global_nav_state.js b/src/legacy/ui/public/chrome/services/global_nav_state.js deleted file mode 100644 index 5a67806852fe8..0000000000000 --- a/src/legacy/ui/public/chrome/services/global_nav_state.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 { distinctUntilChanged } from 'rxjs/operators'; -import { npStart } from 'ui/new_platform'; -import { uiModules } from '../../modules'; - -const newPlatformChrome = npStart.core.chrome; - -uiModules.get('kibana').service('globalNavState', $rootScope => { - let isOpen = false; - newPlatformChrome - .getIsCollapsed$() - .pipe(distinctUntilChanged()) - .subscribe(isCollapsed => { - $rootScope.$evalAsync(() => { - isOpen = !isCollapsed; - $rootScope.$broadcast('globalNavState:change'); - }); - }); - - return { - isOpen: () => isOpen, - - setOpen: newValue => { - newPlatformChrome.setIsCollapsed(!newValue); - }, - }; -}); diff --git a/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap b/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap index 27226eb010ba2..365f3afdab395 100644 --- a/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap +++ b/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap @@ -12,19 +12,45 @@ exports[`is rendered 1`] = `
diff --git a/src/legacy/ui/public/legacy_compat/index.ts b/src/legacy/ui/public/legacy_compat/index.ts index 3b700c8d59399..2067fa6489304 100644 --- a/src/legacy/ui/public/legacy_compat/index.ts +++ b/src/legacy/ui/public/legacy_compat/index.ts @@ -17,7 +17,4 @@ * under the License. */ -export { - configureAppAngularModule, - ensureDefaultIndexPattern, -} from '../../../../plugins/kibana_legacy/public'; +export { configureAppAngularModule } from '../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index ea84ba1ad2838..c58a7d2fbb5cd 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -21,13 +21,15 @@ import sinon from 'sinon'; import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; import { METRIC_TYPE } from '@kbn/analytics'; import { + setFieldFormats, setIndexPatterns, - setQueryService, - setUiSettings, setInjectedMetadata, - setFieldFormats, - setSearchService, + setHttp, + setNotifications, setOverlays, + setQueryService, + setSearchService, + setUiSettings, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -477,11 +479,13 @@ export function __start__(coreStart) { // Services that need to be set in the legacy platform since the legacy data plugin // which previously provided them has been removed. + setHttp(npStart.core.http); + setNotifications(npStart.core.notifications); + setOverlays(npStart.core.overlays); setUiSettings(npStart.core.uiSettings); - setQueryService(npStart.plugins.data.query); - setIndexPatterns(npStart.plugins.data.indexPatterns); setFieldFormats(npStart.plugins.data.fieldFormats); + setIndexPatterns(npStart.plugins.data.indexPatterns); + setQueryService(npStart.plugins.data.query); setSearchService(npStart.plugins.data.search); setAggs(npStart.plugins.data.search.aggs); - setOverlays(npStart.core.overlays); } diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index 498f05457bba9..dd41093f3a1f0 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -20,8 +20,19 @@ jest.mock('history'); import { setRootControllerMock, historyMock } from './new_platform.test.mocks'; -import { legacyAppRegister, __reset__, __setup__ } from './new_platform'; +import { + legacyAppRegister, + __reset__, + __setup__, + __start__, + PluginsSetup, + PluginsStart, +} from './new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import * as dataServices from '../../../../plugins/data/public/services'; +import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public'; import { coreMock } from '../../../../core/public/mocks'; +import { npSetup, npStart } from './__mocks__'; describe('ui/new_platform', () => { describe('legacyAppRegister', () => { @@ -108,4 +119,25 @@ describe('ui/new_platform', () => { expect(unmountMock).toHaveBeenCalled(); }); }); + + describe('service getters', () => { + const services: Record = dataServices; + const getters = Object.keys(services).filter(k => k.substring(0, 3) === 'get'); + + getters.forEach(g => { + it(`sets a value for ${g}`, () => { + __reset__(); + __setup__( + (coreMock.createSetup() as unknown) as LegacyCoreSetup, + (npSetup.plugins as unknown) as PluginsSetup + ); + __start__( + (coreMock.createStart() as unknown) as LegacyCoreStart, + (npStart.plugins as unknown) as PluginsStart + ); + + expect(services[g]()).toBeDefined(); + }); + }); + }); }); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 07e17ad562291..deb8387fee29c 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -31,13 +31,15 @@ import { } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; import { + setFieldFormats, setIndexPatterns, - setQueryService, - setUiSettings, setInjectedMetadata, - setFieldFormats, - setSearchService, + setHttp, + setNotifications, setOverlays, + setQueryService, + setSearchService, + setUiSettings, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; @@ -141,12 +143,14 @@ export function __start__(coreStart: LegacyCoreStart, plugins: PluginsStart) { // Services that need to be set in the legacy platform since the legacy data plugin // which previously provided them has been removed. + setHttp(npStart.core.http); + setNotifications(npStart.core.notifications); + setOverlays(npStart.core.overlays); setUiSettings(npStart.core.uiSettings); - setQueryService(npStart.plugins.data.query); - setIndexPatterns(npStart.plugins.data.indexPatterns); setFieldFormats(npStart.plugins.data.fieldFormats); + setIndexPatterns(npStart.plugins.data.indexPatterns); + setQueryService(npStart.plugins.data.query); setSearchService(npStart.plugins.data.search); - setOverlays(npStart.core.overlays); } /** Flag used to ensure `legacyAppRegister` is only called once. */ diff --git a/src/legacy/ui/public/vis/map/service_settings.js b/src/legacy/ui/public/vis/map/service_settings.js index 233ee526c439b..9f3d21831e3da 100644 --- a/src/legacy/ui/public/vis/map/service_settings.js +++ b/src/legacy/ui/public/vis/map/service_settings.js @@ -47,7 +47,8 @@ uiModules this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), - kbnVersion: kbnVersion, + appVersion: kbnVersion, + appName: 'kibana', fileApiUrl: mapConfig.emsFileApiUrl, tileApiUrl: mapConfig.emsTileApiUrl, htmlSanitizer: $sanitize, diff --git a/src/plugins/console/public/lib/kb/kb.js b/src/plugins/console/public/lib/kb/kb.js index 95896bed02988..053b82bd81d0a 100644 --- a/src/plugins/console/public/lib/kb/kb.js +++ b/src/plugins/console/public/lib/kb/kb.js @@ -147,13 +147,9 @@ function loadApisFromJson( } export function setActiveApi(api) { - if (_.isString(api)) { + if (!api) { $.ajax({ - url: - '../api/console/api_server?sense_version=' + - encodeURIComponent('@@SENSE_VERSION') + - '&apis=' + - encodeURIComponent(api), + url: '../api/console/api_server', dataType: 'json', // disable automatic guessing }).then( function(data) { @@ -169,7 +165,7 @@ export function setActiveApi(api) { ACTIVE_API = api; } -setActiveApi('es_6_0'); +setActiveApi(); export const _test = { loadApisFromJson: loadApisFromJson, diff --git a/src/plugins/console/server/lib/index.ts b/src/plugins/console/server/lib/index.ts index 98004768f880b..2347084b73a66 100644 --- a/src/plugins/console/server/lib/index.ts +++ b/src/plugins/console/server/lib/index.ts @@ -22,4 +22,4 @@ export { ProxyConfigCollection } from './proxy_config_collection'; export { proxyRequest } from './proxy_request'; export { getElasticsearchProxyConfig } from './elasticsearch_proxy_config'; export { setHeaders } from './set_headers'; -export { addProcessorDefinition, addExtensionSpecFilePath } from './spec_definitions'; +export { addProcessorDefinition, addExtensionSpecFilePath, loadSpec } from './spec_definitions'; diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0.js b/src/plugins/console/server/lib/spec_definitions/es.js similarity index 54% rename from src/plugins/console/server/lib/spec_definitions/es_6_0.js rename to src/plugins/console/server/lib/spec_definitions/es.js index 171d232407956..fc24a64f8a6f4 100644 --- a/src/plugins/console/server/lib/spec_definitions/es_6_0.js +++ b/src/plugins/console/server/lib/spec_definitions/es.js @@ -18,26 +18,30 @@ */ import Api from './api'; -import { getSpec } from './spec'; -import { register } from './es_6_0/ingest'; -const ES_6_0 = new Api('es_6_0'); -const spec = getSpec(); +import { getSpec } from './json'; +import { register } from './js/ingest'; +const ES = new Api('es'); -// adding generated specs -Object.keys(spec).forEach(endpoint => { - ES_6_0.addEndpointDescription(endpoint, spec[endpoint]); -}); +export const loadSpec = () => { + const spec = getSpec(); -//adding globals and custom API definitions -require('./es_6_0/aliases')(ES_6_0); -require('./es_6_0/aggregations')(ES_6_0); -require('./es_6_0/document')(ES_6_0); -require('./es_6_0/filter')(ES_6_0); -require('./es_6_0/globals')(ES_6_0); -register(ES_6_0); -require('./es_6_0/mappings')(ES_6_0); -require('./es_6_0/query')(ES_6_0); -require('./es_6_0/reindex')(ES_6_0); -require('./es_6_0/search')(ES_6_0); + // adding generated specs + Object.keys(spec).forEach(endpoint => { + ES.addEndpointDescription(endpoint, spec[endpoint]); + }); -export default ES_6_0; + // adding globals and custom API definitions + require('./js/aliases')(ES); + require('./js/aggregations')(ES); + require('./js/document')(ES); + require('./js/filter')(ES); + require('./js/globals')(ES); + register(ES); + require('./js/mappings')(ES); + require('./js/settings')(ES); + require('./js/query')(ES); + require('./js/reindex')(ES); + require('./js/search')(ES); +}; + +export default ES; diff --git a/src/plugins/console/server/lib/spec_definitions/index.d.ts b/src/plugins/console/server/lib/spec_definitions/index.d.ts index 0a79d3fb386f1..da0125a186c15 100644 --- a/src/plugins/console/server/lib/spec_definitions/index.d.ts +++ b/src/plugins/console/server/lib/spec_definitions/index.d.ts @@ -19,6 +19,13 @@ export declare function addProcessorDefinition(...args: any[]): any; -export declare function resolveApi(senseVersion: string, apis: string[]): object; +export declare function resolveApi(): object; export declare function addExtensionSpecFilePath(...args: any[]): any; + +/** + * A function that synchronously reads files JSON from disk and builds + * the autocomplete structures served to the client. This must be called + * after any extensions have been loaded. + */ +export declare function loadSpec(): any; diff --git a/src/plugins/console/server/lib/spec_definitions/index.js b/src/plugins/console/server/lib/spec_definitions/index.js index 3fe1913d5a193..abf55639fbee8 100644 --- a/src/plugins/console/server/lib/spec_definitions/index.js +++ b/src/plugins/console/server/lib/spec_definitions/index.js @@ -17,8 +17,10 @@ * under the License. */ -export { addProcessorDefinition } from './es_6_0/ingest'; +export { addProcessorDefinition } from './js/ingest'; -export { addExtensionSpecFilePath } from './spec'; +export { addExtensionSpecFilePath } from './json'; + +export { loadSpec } from './es'; export { resolveApi } from './server'; diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/aggregations.js b/src/plugins/console/server/lib/spec_definitions/js/aggregations.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/aggregations.js rename to src/plugins/console/server/lib/spec_definitions/js/aggregations.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/aliases.js b/src/plugins/console/server/lib/spec_definitions/js/aliases.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/aliases.js rename to src/plugins/console/server/lib/spec_definitions/js/aliases.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/document.js b/src/plugins/console/server/lib/spec_definitions/js/document.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/document.js rename to src/plugins/console/server/lib/spec_definitions/js/document.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/filter.js b/src/plugins/console/server/lib/spec_definitions/js/filter.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/filter.js rename to src/plugins/console/server/lib/spec_definitions/js/filter.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/globals.js b/src/plugins/console/server/lib/spec_definitions/js/globals.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/globals.js rename to src/plugins/console/server/lib/spec_definitions/js/globals.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/ingest.js b/src/plugins/console/server/lib/spec_definitions/js/ingest.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/ingest.js rename to src/plugins/console/server/lib/spec_definitions/js/ingest.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js b/src/plugins/console/server/lib/spec_definitions/js/mappings.js similarity index 99% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js rename to src/plugins/console/server/lib/spec_definitions/js/mappings.js index 8c31e5bc6fbb2..5884d14d4dc8b 100644 --- a/src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js +++ b/src/plugins/console/server/lib/spec_definitions/js/mappings.js @@ -19,9 +19,7 @@ const _ = require('lodash'); -const BOOLEAN = { - __one_of: [true, false], -}; +import { BOOLEAN } from './shared'; export default function(api) { api.addEndpointDescription('put_mapping', { diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js b/src/plugins/console/server/lib/spec_definitions/js/query/dsl.js similarity index 99% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js rename to src/plugins/console/server/lib/spec_definitions/js/query/dsl.js index a5f0d15dee0e9..16b952fe0fe4f 100644 --- a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js +++ b/src/plugins/console/server/lib/spec_definitions/js/query/dsl.js @@ -281,9 +281,11 @@ export function queryDsl(api) { __scope_link: '.', }, ], - filter: { - __scope_link: 'GLOBAL.filter', - }, + filter: [ + { + __scope_link: 'GLOBAL.filter', + }, + ], minimum_should_match: 1, boost: 1.0, }, diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/index.js b/src/plugins/console/server/lib/spec_definitions/js/query/index.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/query/index.js rename to src/plugins/console/server/lib/spec_definitions/js/query/index.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/templates.js b/src/plugins/console/server/lib/spec_definitions/js/query/templates.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/query/templates.js rename to src/plugins/console/server/lib/spec_definitions/js/query/templates.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/reindex.js b/src/plugins/console/server/lib/spec_definitions/js/reindex.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/reindex.js rename to src/plugins/console/server/lib/spec_definitions/js/reindex.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/search.js b/src/plugins/console/server/lib/spec_definitions/js/search.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/search.js rename to src/plugins/console/server/lib/spec_definitions/js/search.js diff --git a/src/plugins/console/server/lib/spec_definitions/js/settings.js b/src/plugins/console/server/lib/spec_definitions/js/settings.js new file mode 100644 index 0000000000000..26cd0987c34a5 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/js/settings.js @@ -0,0 +1,74 @@ +/* + * 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 { BOOLEAN } from './shared'; + +export default function(api) { + api.addEndpointDescription('put_settings', { + data_autocomplete_rules: { + refresh_interval: '1s', + number_of_shards: 1, + number_of_replicas: 1, + 'blocks.read_only': BOOLEAN, + 'blocks.read': BOOLEAN, + 'blocks.write': BOOLEAN, + 'blocks.metadata': BOOLEAN, + term_index_interval: 32, + term_index_divisor: 1, + 'translog.flush_threshold_ops': 5000, + 'translog.flush_threshold_size': '200mb', + 'translog.flush_threshold_period': '30m', + 'translog.disable_flush': BOOLEAN, + 'cache.filter.max_size': '2gb', + 'cache.filter.expire': '2h', + 'gateway.snapshot_interval': '10s', + routing: { + allocation: { + include: { + tag: '', + }, + exclude: { + tag: '', + }, + require: { + tag: '', + }, + total_shards_per_node: -1, + }, + }, + 'recovery.initial_shards': { + __one_of: ['quorum', 'quorum-1', 'half', 'full', 'full-1'], + }, + 'ttl.disable_purge': BOOLEAN, + analysis: { + analyzer: {}, + tokenizer: {}, + filter: {}, + char_filter: {}, + }, + 'cache.query.enable': BOOLEAN, + shadow_replicas: BOOLEAN, + shared_filesystem: BOOLEAN, + data_path: 'path', + codec: { + __one_of: ['default', 'best_compression', 'lucene_default'], + }, + }, + }); +} diff --git a/src/legacy/ui/public/indices/index.js b/src/plugins/console/server/lib/spec_definitions/js/shared.js similarity index 81% rename from src/legacy/ui/public/indices/index.js rename to src/plugins/console/server/lib/spec_definitions/js/shared.js index c1646bd66e367..ace189e2d0913 100644 --- a/src/legacy/ui/public/indices/index.js +++ b/src/plugins/console/server/lib/spec_definitions/js/shared.js @@ -17,10 +17,6 @@ * under the License. */ -export { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from './constants'; - -export { - indexNameBeginsWithPeriod, - findIllegalCharactersInIndexName, - indexNameContainsSpaces, -} from './validate'; +export const BOOLEAN = Object.freeze({ + __one_of: [true, false], +}); diff --git a/src/plugins/console/server/lib/spec_definitions/spec/.eslintrc b/src/plugins/console/server/lib/spec_definitions/json/.eslintrc similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/.eslintrc rename to src/plugins/console/server/lib/spec_definitions/json/.eslintrc diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/_common.json b/src/plugins/console/server/lib/spec_definitions/json/generated/_common.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/_common.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/_common.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/bulk.json b/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/bulk.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.aliases.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.aliases.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.aliases.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.aliases.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.allocation.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.allocation.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.allocation.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.allocation.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.count.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.count.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.count.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.count.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.fielddata.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.fielddata.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.fielddata.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.fielddata.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.health.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.health.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.health.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.health.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.help.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.help.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.help.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.help.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.indices.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.indices.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.master.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.master.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.master.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.master.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodeattrs.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodeattrs.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodeattrs.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodeattrs.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodes.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodes.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.pending_tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.pending_tasks.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.pending_tasks.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.pending_tasks.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.plugins.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.plugins.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.recovery.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.recovery.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.recovery.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.recovery.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.repositories.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.repositories.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.repositories.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.repositories.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.segments.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.segments.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.segments.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.segments.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.shards.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.shards.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.snapshots.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.snapshots.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.snapshots.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.snapshots.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.tasks.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.templates.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.templates.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.templates.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.templates.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.thread_pool.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.thread_pool.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/clear_scroll.json b/src/plugins/console/server/lib/spec_definitions/json/generated/clear_scroll.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/clear_scroll.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/clear_scroll.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.allocation_explain.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.allocation_explain.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.allocation_explain.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.allocation_explain.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.get_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.get_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.health.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.health.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.health.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.health.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.pending_tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.pending_tasks.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.pending_tasks.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.pending_tasks.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.put_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.remote_info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.remote_info.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.remote_info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.remote_info.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.reroute.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.reroute.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.reroute.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.reroute.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.state.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.state.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.state.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.state.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.stats.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.stats.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/count.json b/src/plugins/console/server/lib/spec_definitions/json/generated/count.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/count.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/count.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/create.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query_rethrottle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query_rethrottle.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query_rethrottle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query_rethrottle.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_script.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete_script.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete_script.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/exists.json b/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/exists.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/exists.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/exists_source.json b/src/plugins/console/server/lib/spec_definitions/json/generated/exists_source.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/exists_source.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/exists_source.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/explain.json b/src/plugins/console/server/lib/spec_definitions/json/generated/explain.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/explain.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/explain.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/field_caps.json b/src/plugins/console/server/lib/spec_definitions/json/generated/field_caps.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/field_caps.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/field_caps.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_script.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_script.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_context.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_context.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_context.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_script_context.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_languages.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_languages.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_languages.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_script_languages.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_source.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_source.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/index.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/index.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/index.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.analyze.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.analyze.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.analyze.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.analyze.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clear_cache.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.clear_cache.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clear_cache.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.clear_cache.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clone.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.clone.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clone.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.clone.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.close.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.close.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.close.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.close.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.create.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_type.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_type.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_type.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_type.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush_synced.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush_synced.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush_synced.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush_synced.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.forcemerge.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.forcemerge.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.forcemerge.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.forcemerge.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_field_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_field_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_upgrade.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_upgrade.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_upgrade.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_upgrade.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.open.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.open.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.open.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.open.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.recovery.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.recovery.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.recovery.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.recovery.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.refresh.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.refresh.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.refresh.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.refresh.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.rollover.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.rollover.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.segments.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.segments.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.segments.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.segments.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shard_stores.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shard_stores.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shard_stores.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.shard_stores.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shrink.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shrink.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.split.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.split.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.update_aliases.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.update_aliases.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.update_aliases.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.update_aliases.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.upgrade.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.upgrade.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.upgrade.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.upgrade.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.validate_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.validate_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.validate_query.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.validate_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/info.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/info.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.delete_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.delete_pipeline.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.delete_pipeline.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.delete_pipeline.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.get_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.get_pipeline.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.processor_grok.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.processor_grok.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.processor_grok.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.processor_grok.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.put_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.put_pipeline.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.put_pipeline.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.put_pipeline.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.simulate.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.simulate.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.simulate.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.simulate.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/mget.json b/src/plugins/console/server/lib/spec_definitions/json/generated/mget.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/mget.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/mget.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/msearch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/msearch.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/msearch_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/msearch_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/mtermvectors.json b/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/mtermvectors.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.hot_threads.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.hot_threads.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.hot_threads.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.hot_threads.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.info.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.info.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.reload_secure_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.reload_secure_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.reload_secure_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.reload_secure_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.usage.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.usage.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.usage.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.usage.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/put_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/put_script.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/put_script.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/put_script.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/rank_eval.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rank_eval.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/rank_eval.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/rank_eval.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/reindex.json b/src/plugins/console/server/lib/spec_definitions/json/generated/reindex.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/reindex.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/reindex.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/reindex_rethrottle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/reindex_rethrottle.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/reindex_rethrottle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/reindex_rethrottle.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/render_search_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/render_search_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/render_search_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/render_search_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/scripts_painless_execute.json b/src/plugins/console/server/lib/spec_definitions/json/generated/scripts_painless_execute.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/scripts_painless_execute.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/scripts_painless_execute.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/scroll.json b/src/plugins/console/server/lib/spec_definitions/json/generated/scroll.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/scroll.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/scroll.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/search.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/search.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/search.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/search_shards.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search_shards.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/search_shards.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/search_shards.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/search_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/search_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.cleanup_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.cleanup_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.restore.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.restore.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.restore.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.restore.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.status.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.status.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.status.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.status.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.verify_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.verify_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.verify_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.verify_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.cancel.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.cancel.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/tasks.get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.list.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.list.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.list.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/tasks.list.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/termvectors.json b/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/termvectors.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/update.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/update.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/update.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query_rethrottle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query_rethrottle.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query_rethrottle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query_rethrottle.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/index.js b/src/plugins/console/server/lib/spec_definitions/json/index.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/index.js rename to src/plugins/console/server/lib/spec_definitions/json/index.js diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/clear_scroll.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/clear_scroll.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/clear_scroll.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/clear_scroll.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.health.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.health.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.health.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.health.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.put_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.reroute.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.reroute.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.reroute.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.reroute.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/count.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/count.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/count.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/count.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.analyze.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.analyze.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.analyze.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.analyze.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.clone.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.clone.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.clone.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.clone.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.create.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.create.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.delete_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.delete_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.delete_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.delete_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.exists_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.exists_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.exists_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.exists_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_field_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_field_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_field_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_field_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_alias.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_settings.json new file mode 100644 index 0000000000000..2ae8fd82be4d8 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_settings.json @@ -0,0 +1,7 @@ +{ + "indices.put_settings": { + "data_autocomplete_rules": { + "__scope_link": "put_settings" + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.rollover.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.rollover.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.rollover.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.rollover.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.update_aliases.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.update_aliases.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.update_aliases.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.update_aliases.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.validate_query.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.validate_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.validate_query.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.validate_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create_repository.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.restore.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.restore.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.restore.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.restore.json diff --git a/src/plugins/console/server/lib/spec_definitions/server.js b/src/plugins/console/server/lib/spec_definitions/server.js index dd700bf019507..cb855958d403a 100644 --- a/src/plugins/console/server/lib/spec_definitions/server.js +++ b/src/plugins/console/server/lib/spec_definitions/server.js @@ -17,21 +17,10 @@ * under the License. */ -import _ from 'lodash'; +import es from './es'; -const KNOWN_APIS = ['es_6_0']; - -export function resolveApi(senseVersion, apis) { - const result = {}; - _.each(apis, function(name) { - { - if (KNOWN_APIS.includes(name)) { - // for now we ignore sense_version. might add it in the api name later - const api = require('./' + name); // eslint-disable-line import/no-dynamic-require - result[name] = api.asJson(); - } - } - }); - - return result; +export function resolveApi() { + return { + es: es.asJson(), + }; } diff --git a/src/plugins/console/server/lib/spec_definitions/server.test.js b/src/plugins/console/server/lib/spec_definitions/server.test.js deleted file mode 100644 index 747689237c177..0000000000000 --- a/src/plugins/console/server/lib/spec_definitions/server.test.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { resolveApi } from './server'; - -describe('resolveApi', () => { - it('allows known APIs to be resolved', () => { - const mockReply = jest.fn(result => ({ type: () => result })); - const result = resolveApi('Sense Version', ['es_6_0'], { response: mockReply }); - expect(result).toMatchObject({ - es_6_0: { - endpoints: expect.any(Object), - globals: expect.any(Object), - name: expect.any(String), - }, - }); - }); - - it('does not resolve APIs that are not known', () => { - const mockReply = jest.fn(result => ({ type: () => result })); - const result = resolveApi('Sense Version', ['unknown'], { response: mockReply }); - expect(result).toEqual({}); - }); - - it('handles request for apis that are known and unknown', () => { - const mockReply = jest.fn(result => ({ type: () => result })); - const result = resolveApi('Sense Version', ['es_6_0'], { response: mockReply }); - expect(result).toMatchObject({ - es_6_0: { - endpoints: expect.any(Object), - globals: expect.any(Object), - name: expect.any(String), - }, - }); - }); -}); diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json deleted file mode 100644 index 2e1e3024665a4..0000000000000 --- a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "indices.put_settings": { - "data_autocomplete_rules": { - "refresh_interval": "1s", - "number_of_shards": 1, - "number_of_replicas": 1, - "blocks.read_only": { - "__one_of": [ - false, - true - ] - }, - "blocks.read": { - "__one_of": [ - true, - false - ] - }, - "blocks.write": { - "__one_of": [ - true, - false - ] - }, - "blocks.metadata": { - "__one_of": [ - true, - false - ] - }, - "term_index_interval": 32, - "term_index_divisor": 1, - "translog.flush_threshold_ops": 5000, - "translog.flush_threshold_size": "200mb", - "translog.flush_threshold_period": "30m", - "translog.disable_flush": { - "__one_of": [ - true, - false - ] - }, - "cache.filter.max_size": "2gb", - "cache.filter.expire": "2h", - "gateway.snapshot_interval": "10s", - "routing": { - "allocation": { - "include": { - "tag": "" - }, - "exclude": { - "tag": "" - }, - "require": { - "tag": "" - }, - "total_shards_per_node": -1 - } - }, - "recovery.initial_shards": { - "__one_of": [ - "quorum", - "quorum-1", - "half", - "full", - "full-1" - ] - }, - "ttl.disable_purge": { - "__one_of": [ - true, - false - ] - }, - "analysis": { - "analyzer": {}, - "tokenizer": {}, - "filter": {}, - "char_filter": {} - }, - "cache.query.enable": { - "__one_of": [ - true, - false - ] - }, - "shadow_replicas": { - "__one_of": [ - true, - false - ] - }, - "shared_filesystem": { - "__one_of": [ - true, - false - ] - }, - "data_path": "path", - "codec": { - "__one_of": [ - "default", - "best_compression", - "lucene_default" - ] - } - } - } -} diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index 65647bd5acb7c..1954918f4d74f 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -21,7 +21,12 @@ import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/serv import { readLegacyEsConfig } from '../../../legacy/core_plugins/console_legacy'; -import { ProxyConfigCollection, addExtensionSpecFilePath, addProcessorDefinition } from './lib'; +import { + ProxyConfigCollection, + addExtensionSpecFilePath, + addProcessorDefinition, + loadSpec, +} from './lib'; import { ConfigType } from './config'; import { registerProxyRoute } from './routes/api/console/proxy'; import { registerSpecDefinitionsRoute } from './routes/api/console/spec_definitions'; @@ -75,5 +80,7 @@ export class ConsoleServerPlugin implements Plugin { }; } - start() {} + start() { + loadSpec(); + } } diff --git a/src/plugins/console/server/routes/api/console/spec_definitions/index.ts b/src/plugins/console/server/routes/api/console/spec_definitions/index.ts index e2ece37f407ac..88bc250bbfce6 100644 --- a/src/plugins/console/server/routes/api/console/spec_definitions/index.ts +++ b/src/plugins/console/server/routes/api/console/spec_definitions/index.ts @@ -16,33 +16,19 @@ * specific language governing permissions and limitations * under the License. */ -import { schema, TypeOf } from '@kbn/config-schema'; import { IRouter, RequestHandler } from 'kibana/server'; import { resolveApi } from '../../../../lib/spec_definitions'; export const registerSpecDefinitionsRoute = ({ router }: { router: IRouter }) => { - const handler: RequestHandler> = async ( - ctx, - request, - response - ) => { - const { sense_version: version, apis } = request.query; - + const handler: RequestHandler = async (ctx, request, response) => { return response.ok({ - body: resolveApi(version, apis.split(',')), + body: resolveApi(), headers: { 'Content-Type': 'application/json', }, }); }; - const validate = { - query: schema.object({ - sense_version: schema.string({ defaultValue: '' }), - apis: schema.string(), - }), - }; - - router.get({ path: '/api/console/api_server', validate }, handler); - router.post({ path: '/api/console/api_server', validate }, handler); + router.get({ path: '/api/console/api_server', validate: false }, handler); + router.post({ path: '/api/console/api_server', validate: false }, handler); }; diff --git a/src/plugins/data/README.md b/src/plugins/data/README.md index 53618ec049e7c..0fa304c988935 100644 --- a/src/plugins/data/README.md +++ b/src/plugins/data/README.md @@ -6,4 +6,4 @@ - `filter` - `index_patterns` - `query` -- `search` +- `search` \ No newline at end of file diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts similarity index 99% rename from src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts rename to src/plugins/data/common/query/filter_manager/compare_filters.test.ts index da8f5b3564948..b0bb2f754d6cf 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts @@ -18,7 +18,7 @@ */ import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters'; -import { buildEmptyFilter, buildQueryFilter, FilterStateStore } from '../../../../common'; +import { buildEmptyFilter, buildQueryFilter, FilterStateStore } from '../../es_query'; describe('filter manager utilities', () => { describe('compare filters', () => { diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts b/src/plugins/data/common/query/filter_manager/compare_filters.ts similarity index 98% rename from src/plugins/data/public/query/filter_manager/lib/compare_filters.ts rename to src/plugins/data/common/query/filter_manager/compare_filters.ts index a2105fdc1d3ef..e047d5e0665d5 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.ts @@ -18,7 +18,7 @@ */ import { defaults, isEqual, omit, map } from 'lodash'; -import { FilterMeta, Filter } from '../../../../common'; +import { FilterMeta, Filter } from '../../es_query'; export interface FilterCompareOptions { disabled?: boolean; diff --git a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.test.ts b/src/plugins/data/common/query/filter_manager/dedup_filters.test.ts similarity index 95% rename from src/plugins/data/public/query/filter_manager/lib/dedup_filters.test.ts rename to src/plugins/data/common/query/filter_manager/dedup_filters.test.ts index ecc0ec94e07c8..228489de37daa 100644 --- a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/dedup_filters.test.ts @@ -18,14 +18,8 @@ */ import { dedupFilters } from './dedup_filters'; -import { - Filter, - IIndexPattern, - IFieldType, - buildRangeFilter, - buildQueryFilter, - FilterStateStore, -} from '../../../../common'; +import { Filter, buildRangeFilter, buildQueryFilter, FilterStateStore } from '../../es_query'; +import { IIndexPattern, IFieldType } from '../../index_patterns'; describe('filter manager utilities', () => { let indexPattern: IIndexPattern; diff --git a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts b/src/plugins/data/common/query/filter_manager/dedup_filters.ts similarity index 97% rename from src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts rename to src/plugins/data/common/query/filter_manager/dedup_filters.ts index d5d0e70504b41..7d1b00ac10c0d 100644 --- a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts +++ b/src/plugins/data/common/query/filter_manager/dedup_filters.ts @@ -19,7 +19,7 @@ import { filter, find } from 'lodash'; import { compareFilters, FilterCompareOptions } from './compare_filters'; -import { Filter } from '../../../../common'; +import { Filter } from '../../es_query'; /** * Combine 2 filter collections, removing duplicates diff --git a/src/plugins/data/common/query/filter_manager/index.ts b/src/plugins/data/common/query/filter_manager/index.ts new file mode 100644 index 0000000000000..315c124f083a8 --- /dev/null +++ b/src/plugins/data/common/query/filter_manager/index.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +export { dedupFilters } from './dedup_filters'; +export { uniqFilters } from './uniq_filters'; +export { compareFilters, COMPARE_ALL_OPTIONS, FilterCompareOptions } from './compare_filters'; diff --git a/src/plugins/data/public/query/filter_manager/lib/uniq_filters.test.ts b/src/plugins/data/common/query/filter_manager/uniq_filters.test.ts similarity index 99% rename from src/plugins/data/public/query/filter_manager/lib/uniq_filters.test.ts rename to src/plugins/data/common/query/filter_manager/uniq_filters.test.ts index 8b525a3d2a2e4..5a35e85c95eaa 100644 --- a/src/plugins/data/public/query/filter_manager/lib/uniq_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/uniq_filters.test.ts @@ -18,7 +18,7 @@ */ import { uniqFilters } from './uniq_filters'; -import { buildQueryFilter, Filter, FilterStateStore } from '../../../../common'; +import { buildQueryFilter, Filter, FilterStateStore } from '../../es_query'; describe('filter manager utilities', () => { describe('niqFilter', () => { diff --git a/src/plugins/data/public/query/filter_manager/lib/uniq_filters.ts b/src/plugins/data/common/query/filter_manager/uniq_filters.ts similarity index 96% rename from src/plugins/data/public/query/filter_manager/lib/uniq_filters.ts rename to src/plugins/data/common/query/filter_manager/uniq_filters.ts index 44c102d7ab15d..683cbf7c78a89 100644 --- a/src/plugins/data/public/query/filter_manager/lib/uniq_filters.ts +++ b/src/plugins/data/common/query/filter_manager/uniq_filters.ts @@ -17,8 +17,8 @@ * under the License. */ import { each, union } from 'lodash'; +import { Filter } from '../../es_query'; import { dedupFilters } from './dedup_filters'; -import { Filter } from '../../../../common'; /** * Remove duplicate filters from an array of filters diff --git a/src/plugins/data/common/query/index.ts b/src/plugins/data/common/query/index.ts index d8f7b5091eb8f..421cc4f63e4ef 100644 --- a/src/plugins/data/common/query/index.ts +++ b/src/plugins/data/common/query/index.ts @@ -17,4 +17,5 @@ * under the License. */ +export * from './filter_manager'; export * from './types'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 58bd9a5ab05d7..339a5fea91c5f 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -47,13 +47,13 @@ import { isQueryStringFilter, isRangeFilter, toggleFilterNegated, + compareFilters, + COMPARE_ALL_OPTIONS, } from '../common'; import { FilterLabel } from './ui/filter_bar'; import { - compareFilters, - COMPARE_ALL_OPTIONS, generateFilters, onlyDisabledFiltersChanged, changeTimeFilter, diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts index b6ca91169a933..305aa8575e4d7 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts @@ -18,6 +18,8 @@ */ import { defaults, pluck, last, get } from 'lodash'; + +jest.mock('../../../../kibana_utils/public/history'); import { IndexPattern } from './index_pattern'; import { DuplicateField } from '../../../../kibana_utils/public'; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index a01c133712206..fc5dde94fa851 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -39,6 +39,7 @@ import { createIndexPatternSelect } from './ui/index_pattern_select'; import { IndexPatternsService } from './index_patterns'; import { setFieldFormats, + setHttp, setIndexPatterns, setInjectedMetadata, setNotifications, @@ -128,6 +129,7 @@ export class DataPublicPlugin implements Plugin string; getPhraseFilterValue: (filter: import("../common").PhraseFilter) => string | number | boolean; getDisplayValueFromFilter: typeof getDisplayValueFromFilter; - compareFilters: (first: import("../common").Filter | import("../common").Filter[], second: import("../common").Filter | import("../common").Filter[], comparatorOptions?: import("./query/filter_manager/lib/compare_filters").FilterCompareOptions) => boolean; - COMPARE_ALL_OPTIONS: import("./query/filter_manager/lib/compare_filters").FilterCompareOptions; + compareFilters: (first: import("../common").Filter | import("../common").Filter[], second: import("../common").Filter | import("../common").Filter[], comparatorOptions?: import("../common").FilterCompareOptions) => boolean; + COMPARE_ALL_OPTIONS: import("../common").FilterCompareOptions; generateFilters: typeof generateFilters; onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; @@ -1843,8 +1843,8 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:38:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:60:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index c951953b26555..fba1866ebd615 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -22,13 +22,19 @@ import { Subject } from 'rxjs'; import { IUiSettingsClient } from 'src/core/public'; -import { COMPARE_ALL_OPTIONS, compareFilters } from './lib/compare_filters'; import { sortFilters } from './lib/sort_filters'; import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; -import { uniqFilters } from './lib/uniq_filters'; import { onlyDisabledFiltersChanged } from './lib/only_disabled'; import { PartitionedFilters } from './types'; -import { FilterStateStore, Filter, isFilterPinned } from '../../../common'; + +import { + FilterStateStore, + Filter, + uniqFilters, + isFilterPinned, + compareFilters, + COMPARE_ALL_OPTIONS, +} from '../../../common'; export class FilterManager { private filters: Filter[] = []; diff --git a/src/plugins/data/public/query/filter_manager/index.ts b/src/plugins/data/public/query/filter_manager/index.ts index 09990adacde45..be512c503d531 100644 --- a/src/plugins/data/public/query/filter_manager/index.ts +++ b/src/plugins/data/public/query/filter_manager/index.ts @@ -19,8 +19,6 @@ export { FilterManager } from './filter_manager'; -export { uniqFilters } from './lib/uniq_filters'; export { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; export { onlyDisabledFiltersChanged } from './lib/only_disabled'; export { generateFilters } from './lib/generate_filters'; -export { compareFilters, COMPARE_ALL_OPTIONS } from './lib/compare_filters'; diff --git a/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts b/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts index 34e1ac38ae95f..18c51ebeabe54 100644 --- a/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts +++ b/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts @@ -18,8 +18,7 @@ */ import { filter } from 'lodash'; -import { Filter } from '../../../../common'; -import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters'; +import { Filter, compareFilters, COMPARE_ALL_OPTIONS } from '../../../../common'; const isEnabled = (f: Filter) => f && f.meta && !f.meta.disabled; diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts index a22e66860c765..331d8969f2483 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts @@ -21,10 +21,9 @@ import { Subscription } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import _ from 'lodash'; import { BaseStateContainer } from '../../../../kibana_utils/public'; -import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; import { QuerySetup, QueryStart } from '../query_service'; import { QueryState, QueryStateChange } from './types'; -import { FilterStateStore } from '../../../common/es_query/filters'; +import { FilterStateStore, COMPARE_ALL_OPTIONS, compareFilters } from '../../../common'; /** * Helper to setup two-way syncing of global data and a state container diff --git a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts index d0d97bfaaeb36..dd075f9be7d94 100644 --- a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts +++ b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts @@ -20,10 +20,10 @@ import { Observable, Subscription } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { TimefilterSetup } from '../timefilter'; -import { COMPARE_ALL_OPTIONS, compareFilters, FilterManager } from '../filter_manager'; +import { FilterManager } from '../filter_manager'; import { QueryState, QueryStateChange } from './index'; import { createStateContainer } from '../../../../kibana_utils/public'; -import { isFilterPinned } from '../../../common/es_query/filters'; +import { isFilterPinned, compareFilters, COMPARE_ALL_OPTIONS } from '../../../common'; export function createQueryStateObservable({ timefilter: { timefilter }, diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 03dbd40984412..b7569a22e9fc9 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -39,7 +39,7 @@ export function registerValueSuggestionsRoute( { index: schema.string(), }, - { allowUnknowns: false } + { unknowns: 'allow' } ), body: schema.object( { @@ -47,7 +47,7 @@ export function registerValueSuggestionsRoute( query: schema.string(), boolFilter: schema.maybe(schema.any()), }, - { allowUnknowns: false } + { unknowns: 'allow' } ), }, }, diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 0165486fc2de7..5038b4226fad8 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -166,7 +166,7 @@ export { ParsedInterval } from '../common'; export { ISearch, - ICancel, + ISearchCancel, ISearchOptions, IRequestTypesMap, IResponseTypesMap, diff --git a/src/plugins/data/server/search/i_route_handler_search_context.ts b/src/plugins/data/server/search/i_route_handler_search_context.ts index 89862781b826e..9888c774ea104 100644 --- a/src/plugins/data/server/search/i_route_handler_search_context.ts +++ b/src/plugins/data/server/search/i_route_handler_search_context.ts @@ -17,9 +17,9 @@ * under the License. */ -import { ISearchGeneric, ICancelGeneric } from './i_search'; +import { ISearchGeneric, ISearchCancelGeneric } from './i_search'; export interface IRouteHandlerSearchContext { search: ISearchGeneric; - cancel: ICancelGeneric; + cancel: ISearchCancelGeneric; } diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts index ea014c5e136d9..fa4aa72ac7287 100644 --- a/src/plugins/data/server/search/i_search.ts +++ b/src/plugins/data/server/search/i_search.ts @@ -42,7 +42,7 @@ export type ISearchGeneric = Promise; -export type ICancelGeneric = ( +export type ISearchCancelGeneric = ( id: string, strategy?: T ) => Promise; @@ -52,4 +52,4 @@ export type ISearch = ( options?: ISearchOptions ) => Promise; -export type ICancel = (id: string) => Promise; +export type ISearchCancel = (id: string) => Promise; diff --git a/src/plugins/data/server/search/i_search_strategy.ts b/src/plugins/data/server/search/i_search_strategy.ts index 4cfc9608383a9..9b405034f883f 100644 --- a/src/plugins/data/server/search/i_search_strategy.ts +++ b/src/plugins/data/server/search/i_search_strategy.ts @@ -18,7 +18,7 @@ */ import { APICaller } from 'kibana/server'; -import { ISearch, ICancel, ISearchGeneric } from './i_search'; +import { ISearch, ISearchCancel, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; import { ISearchContext } from './i_search_context'; @@ -28,7 +28,7 @@ import { ISearchContext } from './i_search_context'; */ export interface ISearchStrategy { search: ISearch; - cancel?: ICancel; + cancel?: ISearchCancel; } /** diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 385e96ee803b6..15738a3befb27 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -21,7 +21,13 @@ export { ISearchSetup } from './i_search_setup'; export { ISearchContext } from './i_search_context'; -export { ISearch, ICancel, ISearchOptions, IRequestTypesMap, IResponseTypesMap } from './i_search'; +export { + ISearch, + ISearchCancel, + ISearchOptions, + IRequestTypesMap, + IResponseTypesMap, +} from './i_search'; export { TStrategyTypes } from './strategy_types'; diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index e618f99084aed..b90d7d4ff80ce 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -28,9 +28,9 @@ export function registerSearchRoute(router: IRouter): void { validate: { params: schema.object({ strategy: schema.string() }), - query: schema.object({}, { allowUnknowns: true }), + query: schema.object({}, { unknowns: 'allow' }), - body: schema.object({}, { allowUnknowns: true }), + body: schema.object({}, { unknowns: 'allow' }), }, }, async (context, request, res) => { @@ -64,7 +64,7 @@ export function registerSearchRoute(router: IRouter): void { id: schema.string(), }), - query: schema.object({}, { allowUnknowns: true }), + query: schema.object({}, { unknowns: 'allow' }), }, }, async (context, request, res) => { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 2a2d9bb414c14..178b2949a9456 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -329,12 +329,6 @@ export function getDefaultSearchParams(config: SharedGlobalConfig): { restTotalHitsAsInt: boolean; }; -// Warning: (ae-forgotten-export) The symbol "TStrategyTypes" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ICancel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ICancel = (id: string) => Promise; - // Warning: (ae-missing-release-tag) "IFieldFormatsRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -507,11 +501,17 @@ export interface IResponseTypesMap { [ES_SEARCH_STRATEGY]: IEsSearchResponse; } +// Warning: (ae-forgotten-export) The symbol "TStrategyTypes" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ISearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export type ISearch = (request: IRequestTypesMap[T], options?: ISearchOptions) => Promise; +// Warning: (ae-missing-release-tag) "ISearchCancel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ISearchCancel = (id: string) => Promise; + // Warning: (ae-missing-release-tag) "ISearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 2925e5e16458e..8ed01b9b61c7e 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -27,3 +27,5 @@ export { sendRequest, useRequest, } from './request/np_ready_request'; + +export { indices } from './indices'; diff --git a/src/legacy/ui/public/indices/constants/index.js b/src/plugins/es_ui_shared/public/indices/constants/index.ts similarity index 94% rename from src/legacy/ui/public/indices/constants/index.js rename to src/plugins/es_ui_shared/public/indices/constants/index.ts index 72ecc2e4c87de..825975fa161b5 100644 --- a/src/legacy/ui/public/indices/constants/index.js +++ b/src/plugins/es_ui_shared/public/indices/constants/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { indexPatterns } from '../../../../../plugins/data/public'; +import { indexPatterns } from '../../../../data/public'; export const INDEX_ILLEGAL_CHARACTERS_VISIBLE = [...indexPatterns.ILLEGAL_CHARACTERS_VISIBLE, '*']; diff --git a/src/plugins/es_ui_shared/public/indices/index.ts b/src/plugins/es_ui_shared/public/indices/index.ts new file mode 100644 index 0000000000000..a6d279a5c2b4f --- /dev/null +++ b/src/plugins/es_ui_shared/public/indices/index.ts @@ -0,0 +1,33 @@ +/* + * 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 { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from './constants'; + +import { + indexNameBeginsWithPeriod, + findIllegalCharactersInIndexName, + indexNameContainsSpaces, +} from './validate'; + +export const indices = { + INDEX_ILLEGAL_CHARACTERS_VISIBLE, + indexNameBeginsWithPeriod, + findIllegalCharactersInIndexName, + indexNameContainsSpaces, +}; diff --git a/src/legacy/ui/public/indices/validate/index.js b/src/plugins/es_ui_shared/public/indices/validate/index.ts similarity index 100% rename from src/legacy/ui/public/indices/validate/index.js rename to src/plugins/es_ui_shared/public/indices/validate/index.ts diff --git a/src/legacy/ui/public/indices/validate/validate_index.test.js b/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts similarity index 100% rename from src/legacy/ui/public/indices/validate/validate_index.test.js rename to src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts diff --git a/src/legacy/ui/public/indices/validate/validate_index.js b/src/plugins/es_ui_shared/public/indices/validate/validate_index.ts similarity index 67% rename from src/legacy/ui/public/indices/validate/validate_index.js rename to src/plugins/es_ui_shared/public/indices/validate/validate_index.ts index 5deaa83a807d9..00ac1342400ac 100644 --- a/src/legacy/ui/public/indices/validate/validate_index.js +++ b/src/plugins/es_ui_shared/public/indices/validate/validate_index.ts @@ -19,23 +19,29 @@ import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from '../constants'; -// Names beginning with periods are reserved for system indices. -export function indexNameBeginsWithPeriod(indexName = '') { +// Names beginning with periods are reserved for hidden indices. +export function indexNameBeginsWithPeriod(indexName?: string): boolean { + if (indexName === undefined) { + return false; + } return indexName[0] === '.'; } -export function findIllegalCharactersInIndexName(indexName) { - const illegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => { - if (indexName.includes(char)) { - chars.push(char); - } +export function findIllegalCharactersInIndexName(indexName: string): string[] { + const illegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce( + (chars: string[], char: string): string[] => { + if (indexName.includes(char)) { + chars.push(char); + } - return chars; - }, []); + return chars; + }, + [] + ); return illegalCharacters; } -export function indexNameContainsSpaces(indexName) { +export function indexNameContainsSpaces(indexName: string): boolean { return indexName.includes(' '); } diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_name.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_name.ts index 524cac27341ab..5e969fa715172 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_name.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_name.ts @@ -17,14 +17,11 @@ * under the License. */ -// Note: we can't import from "ui/indices" as the TS Type definition don't exist -// import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; +import { indices } from '../../../../public'; import { ValidationFunc } from '../../hook_form_lib'; import { startsWith, containsChars } from '../../../validators/string'; import { ERROR_CODE } from './types'; -const INDEX_ILLEGAL_CHARACTERS = ['\\', '/', '?', '"', '<', '>', '|', '*']; - export const indexNameField = (i18n: any) => ( ...args: Parameters ): ReturnType> => { @@ -51,7 +48,9 @@ export const indexNameField = (i18n: any) => ( }; } - const { charsFound, doesContain } = containsChars(INDEX_ILLEGAL_CHARACTERS)(value as string); + const { charsFound, doesContain } = containsChars(indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE)( + value as string + ); if (doesContain) { return { message: i18n.translate('esUi.forms.fieldValidation.indexNameInvalidCharactersError', { diff --git a/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts b/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts index 4092dfbba00d5..b8be273d7bbd3 100644 --- a/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts +++ b/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - +import { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../../expression_functions'; import { KibanaContext } from '../../expression_types'; +import { Query, uniqFilters } from '../../../../data/common'; interface Arguments { q?: string | null; @@ -35,6 +36,15 @@ export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition< Promise >; +const getParsedValue = (data: any, defaultValue: any) => + typeof data === 'string' && data.length ? JSON.parse(data) || defaultValue : defaultValue; + +const mergeQueries = (first: Query | Query[] = [], second: Query | Query[]) => + uniq( + [...(Array.isArray(first) ? first : [first]), ...(Array.isArray(second) ? second : [second])], + (n: any) => JSON.stringify(n.query) + ); + export const kibanaContextFunction: ExpressionFunctionKibanaContext = { name: 'kibana_context', type: 'kibana_context', @@ -75,9 +85,9 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { }, async fn(input, args, { getSavedObject }) { - const queryArg = args.q ? JSON.parse(args.q) : []; - let queries = Array.isArray(queryArg) ? queryArg : [queryArg]; - let filters = args.filters ? JSON.parse(args.filters) : []; + const timeRange = getParsedValue(args.timeRange, input?.timeRange); + let queries = mergeQueries(input?.query, getParsedValue(args?.q, [])); + let filters = [...(input?.filters || []), ...getParsedValue(args?.filters, [])]; if (args.savedSearchId) { if (typeof getSavedObject !== 'function') { @@ -89,29 +99,20 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { } const obj = await getSavedObject('search', args.savedSearchId); const search = obj.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string }; - const data = JSON.parse(search.searchSourceJSON) as { query: string; filter: any[] }; - queries = queries.concat(data.query); - filters = filters.concat(data.filter); - } + const { query, filter } = getParsedValue(search.searchSourceJSON, {}); - if (input && input.query) { - queries = queries.concat(input.query); - } - - if (input && input.filters) { - filters = filters.concat(input.filters).filter((f: any) => !f.meta.disabled); + if (query) { + queries = mergeQueries(queries, query); + } + if (filter) { + filters = [...filters, ...(Array.isArray(filter) ? filter : [filter])]; + } } - const timeRange = args.timeRange - ? JSON.parse(args.timeRange) - : input - ? input.timeRange - : undefined; - return { type: 'kibana_context', query: queries, - filters, + filters: uniqFilters(filters).filter((f: any) => !f.meta?.disabled), timeRange, }; }, diff --git a/src/plugins/kibana_legacy/public/angular/index.ts b/src/plugins/kibana_legacy/public/angular/index.ts index 5fc37ac39612a..16bae6c4cffe0 100644 --- a/src/plugins/kibana_legacy/public/angular/index.ts +++ b/src/plugins/kibana_legacy/public/angular/index.ts @@ -21,7 +21,6 @@ export { PromiseServiceCreator } from './promises'; // @ts-ignore export { watchMultiDecorator } from './watch_multi'; export * from './angular_config'; -export { ensureDefaultIndexPattern } from './ensure_default_index_pattern'; // @ts-ignore export { createTopNavDirective, createTopNavHelper, loadKbnTopNavDirectives } from './kbn_top_nav'; export { subscribeWithScope } from './subscribe_with_scope'; diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap b/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap index 39bd66ff71c61..ee97a5acfd3d2 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap +++ b/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap @@ -17,27 +17,88 @@ exports[`is rendered 1`] = ` diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss b/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss index e810fe0ccdba6..a2e951cb5b775 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss +++ b/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss @@ -4,66 +4,40 @@ */ .dshExitFullScreenButton { - height: $euiSizeXXL; - left: 0; - bottom: 0; + @include euiBottomShadow; + + left: $euiSizeS; + bottom: $euiSizeS; position: fixed; display: block; padding: 0; border: none; background: none; z-index: 5; + background: $euiColorFullShade; + padding: $euiSizeXS; + border-radius: $euiBorderRadius; + text-align: left; - &:hover, - &:focus { - transition: all $euiAnimSpeedExtraSlow $euiAnimSlightResistance; - z-index: 10 !important; /* 1 */ + &:hover { + background: $euiColorFullShade; - .dshExitFullScreenButton__text { - transition: all $euiAnimSpeedNormal $euiAnimSlightResistance; - transform: translateX(-$euiSize); + .dshExitFullScreenButton__icon { + color: $euiColorEmptyShade; } } } -.dshExitFullScreenButton__logo { - display: block; - // Just darken the background for all themes because the logo is always white - background-color: shade($euiColorPrimary, 25%); - height: $euiSizeXXL; - - // These numbers are very specific to the Kibana logo size - width: 92px; - background-image: url('ui/assets/images/kibana.svg'); - background-position: 8px 5px; - background-size: 72px 30px; - background-repeat: no-repeat; - - z-index: $euiZLevel1; +.dshExitFullScreenButton__title { + line-height: 1.2; + color: $euiColorEmptyShade; } -/** - * 1. Calc made to allow caret in text to peek out / animate. - */ - .dshExitFullScreenButton__text { - background: $euiColorPrimary; - color: $euiColorEmptyShade; - line-height: $euiSizeXXL; - display: inline-block; - font-size: $euiFontSizeS; - height: $euiSizeXXL; - position: absolute; - left: calc(100% + #{$euiSize}); /* 1 */ - top: 0px; - bottom: 0px; - white-space: nowrap; - padding: 0px $euiSizeXS 0px $euiSizeM; - transition: all .2s ease; - transform: translateX(-100%); - z-index: -1; - - .euiIcon { - margin-left: $euiSizeXS; - } + line-height: 1.2; + color: makeHighContrastColor($euiColorMediumShade, $euiColorFullShade); +} + +.dshExitFullScreenButton__icon { + color: makeHighContrastColor($euiColorMediumShade, $euiColorFullShade); } diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx index 5ce508ec1ed5b..97fc02ac64e12 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx +++ b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import React, { PureComponent } from 'react'; import { EuiScreenReaderOnly, keyCodes } from '@elastic/eui'; -import { EuiIcon } from '@elastic/eui'; +import { EuiIcon, EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; export interface ExitFullScreenButtonProps { onExitFullScreenMode: () => void; @@ -61,17 +61,40 @@ class ExitFullScreenButtonUi extends PureComponent { )} className="dshExitFullScreenButton" onClick={this.props.onExitFullScreenMode} + data-test-subj="exitFullScreenModeLogo" > - - - {i18n.translate('kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel', { - defaultMessage: 'Exit full screen', - })} - - + + + + + +
+ +

+ {i18n.translate( + 'kibana-react.exitFullScreenButton.exitFullScreenModeButtonTitle', + { + defaultMessage: 'Elastic Kibana', + } + )} +

+
+ +

+ {i18n.translate( + 'kibana-react.exitFullScreenButton.exitFullScreenModeButtonText', + { + defaultMessage: 'Exit full screen', + } + )} +

+
+
+
+ + + +
diff --git a/src/plugins/kibana_legacy/public/angular/ensure_default_index_pattern.tsx b/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx similarity index 67% rename from src/plugins/kibana_legacy/public/angular/ensure_default_index_pattern.tsx rename to src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx index 1a3bb84ae7575..7992f650cb372 100644 --- a/src/plugins/kibana_legacy/public/angular/ensure_default_index_pattern.tsx +++ b/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx @@ -18,14 +18,13 @@ */ import { contains } from 'lodash'; -import { IRootScopeService } from 'angular'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { History } from 'history'; import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; import { EuiCallOut } from '@elastic/eui'; import { CoreStart } from 'kibana/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { toMountPoint } from '../../../kibana_react/public'; let bannerId: string; let timeoutId: NodeJS.Timeout | undefined; @@ -39,18 +38,17 @@ let timeoutId: NodeJS.Timeout | undefined; * resolve to wait for the URL change to happen. */ export async function ensureDefaultIndexPattern( - newPlatform: CoreStart, + core: CoreStart, data: DataPublicPluginStart, - $rootScope: IRootScopeService, - kbnUrl: any + history: History ) { const patterns = await data.indexPatterns.getIds(); - let defaultId = newPlatform.uiSettings.get('defaultIndex'); + let defaultId = core.uiSettings.get('defaultIndex'); let defined = !!defaultId; const exists = contains(patterns, defaultId); if (defined && !exists) { - newPlatform.uiSettings.remove('defaultIndex'); + core.uiSettings.remove('defaultIndex'); defaultId = defined = false; } @@ -61,10 +59,9 @@ export async function ensureDefaultIndexPattern( // If there is any index pattern created, set the first as default if (patterns.length >= 1) { defaultId = patterns[0]; - newPlatform.uiSettings.set('defaultIndex', defaultId); + core.uiSettings.set('defaultIndex', defaultId); } else { - const canManageIndexPatterns = - newPlatform.application.capabilities.management.kibana.index_patterns; + const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; if (timeoutId) { @@ -73,31 +70,27 @@ export async function ensureDefaultIndexPattern( // Avoid being hostile to new users who don't have an index pattern setup yet // give them a friendly info message instead of a terse error message - bannerId = newPlatform.overlays.banners.replace(bannerId, (element: HTMLElement) => { - ReactDOM.render( - - - , - element - ); - return () => ReactDOM.unmountComponentAtNode(element); - }); + bannerId = 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 timeoutId = setTimeout(() => { - newPlatform.overlays.banners.remove(bannerId); + core.overlays.banners.remove(bannerId); timeoutId = undefined; }, 15000); - kbnUrl.change(redirectTarget); - $rootScope.$digest(); + history.push(redirectTarget); // return never-resolving promise to stop resolving and wait for the url change return new Promise(() => {}); diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index b4b5658c1c886..1a73bbb6b04a1 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -18,3 +18,5 @@ */ export { removeQueryParam } from './remove_query_param'; +export { redirectWhenMissing } from './redirect_when_missing'; +export { ensureDefaultIndexPattern } from './ensure_default_index_pattern'; diff --git a/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx b/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx new file mode 100644 index 0000000000000..cbdeef6fbe96c --- /dev/null +++ b/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx @@ -0,0 +1,80 @@ +/* + * 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 from 'react'; +import { History } from 'history'; +import { i18n } from '@kbn/i18n'; + +import { ToastsSetup } from 'kibana/public'; +import { MarkdownSimple, toMountPoint } from '../../../kibana_react/public'; +import { SavedObjectNotFound } from '../errors'; + +interface Mapping { + [key: string]: string; +} + +/** + * Creates an error handler that will redirect to a url when a SavedObjectNotFound + * error is thrown + */ +export function redirectWhenMissing({ + history, + mapping, + toastNotifications, +}: { + history: History; + /** + * a mapping of url's to redirect to based on the saved object that + * couldn't be found, or just a string that will be used for all types + */ + mapping: string | Mapping; + /** + * Toast notifications service to show toasts in error cases. + */ + toastNotifications: ToastsSetup; +}) { + let localMappingObject: Mapping; + + if (typeof mapping === 'string') { + localMappingObject = { '*': mapping }; + } else { + localMappingObject = mapping; + } + + return (error: SavedObjectNotFound) => { + // if this error is not "404", rethrow + // we can't check "error instanceof SavedObjectNotFound" since this class can live in a separate bundle + // and the error will be an instance of other class with the same interface (actually the copy of SavedObjectNotFound class) + if (!error.savedObjectType) { + throw error; + } + + let url = localMappingObject[error.savedObjectType] || localMappingObject['*'] || '/'; + url += (url.indexOf('?') >= 0 ? '&' : '?') + `notFound=${error.savedObjectType}`; + + toastNotifications.addWarning({ + title: i18n.translate('kibana_utils.history.savedObjectIsMissingNotificationMessage', { + defaultMessage: 'Saved object is missing', + }), + text: toMountPoint({error.message}), + }); + + history.replace(url); + }; +} diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index ee38d5e8111c9..1876e688c989a 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -73,5 +73,5 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; -export { removeQueryParam } from './history'; +export { removeQueryParam, redirectWhenMissing, ensureDefaultIndexPattern } from './history'; export { applyDiff } from './state_management/utils/diff_object'; diff --git a/src/plugins/timelion/config.ts b/src/plugins/timelion/config.ts index 561fb4de9f58d..eaea1aaca1b7b 100644 --- a/src/plugins/timelion/config.ts +++ b/src/plugins/timelion/config.ts @@ -25,7 +25,7 @@ export const configSchema = schema.object( graphiteUrls: schema.maybe(schema.arrayOf(schema.string())), }, // This option should be removed as soon as we entirely migrate config from legacy Timelion plugin. - { allowUnknowns: true } + { unknowns: 'allow' } ); export type ConfigSchema = TypeOf; diff --git a/src/plugins/timelion/server/routes/run.ts b/src/plugins/timelion/server/routes/run.ts index b7a4179da768e..b773bba68ea81 100644 --- a/src/plugins/timelion/server/routes/run.ts +++ b/src/plugins/timelion/server/routes/run.ts @@ -78,15 +78,11 @@ export function runRoute( es: schema.object({ filter: schema.object({ bool: schema.object({ - filter: schema.maybe( - schema.arrayOf(schema.object({}, { allowUnknowns: true })) - ), - must: schema.maybe(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - should: schema.maybe( - schema.arrayOf(schema.object({}, { allowUnknowns: true })) - ), + filter: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + should: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), must_not: schema.maybe( - schema.arrayOf(schema.object({}, { allowUnknowns: true })) + schema.arrayOf(schema.object({}, { unknowns: 'allow' })) ), }), }), diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index e2d1e4d114ad5..9abbc4ad617dc 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -23,7 +23,7 @@ import { getVisData, GetVisDataOptions } from '../lib/get_vis_data'; import { visPayloadSchema } from './post_vis_schema'; import { Framework, ValidationTelemetryServiceSetup } from '../index'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const visDataRoutes = ( router: IRouter, diff --git a/src/setup_node_env/exit_on_warning.js b/src/setup_node_env/exit_on_warning.js index 5be5ccd72bd02..6321cd7ba8db0 100644 --- a/src/setup_node_env/exit_on_warning.js +++ b/src/setup_node_env/exit_on_warning.js @@ -35,4 +35,16 @@ if (process.noProcessWarnings !== true) { process.exit(1); }); + + // While the above warning listener would also be called on + // unhandledRejection warnings, we can give a better error message if we + // handle them separately: + process.on('unhandledRejection', function(reason) { + console.error('Unhandled Promise rejection detected:'); + console.error(); + console.error(reason); + console.error(); + console.error('Terminating process...'); + process.exit(1); + }); } diff --git a/test/common/services/security/role.ts b/test/common/services/security/role.ts index 0e7572882f80d..dfc6ff9b164e5 100644 --- a/test/common/services/security/role.ts +++ b/test/common/services/security/role.ts @@ -43,7 +43,6 @@ export class Role { `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` ); } - this.log.debug(`created role ${name}`); } public async delete(name: string) { @@ -56,6 +55,5 @@ export class Role { )}` ); } - this.log.debug(`deleted role ${name}`); } } diff --git a/test/common/services/security/security.ts b/test/common/services/security/security.ts index 4eebb7b6697e0..6ad0933a2a5a2 100644 --- a/test/common/services/security/security.ts +++ b/test/common/services/security/security.ts @@ -23,15 +23,21 @@ import { Role } from './role'; import { User } from './user'; import { RoleMappings } from './role_mappings'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { createTestUserService } from './test_user'; -export function SecurityServiceProvider({ getService }: FtrProviderContext) { +export async function SecurityServiceProvider(context: FtrProviderContext) { + const { getService } = context; const log = getService('log'); const config = getService('config'); const url = formatUrl(config.get('servers.kibana')); + const role = new Role(url, log); + const user = new User(url, log); + const testUser = await createTestUserService(role, user, context); return new (class SecurityService { - role = new Role(url, log); roleMappings = new RoleMappings(url, log); - user = new User(url, log); + testUser = testUser; + role = role; + user = user; })(); } diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts new file mode 100644 index 0000000000000..7f01c64d291a5 --- /dev/null +++ b/test/common/services/security/test_user.ts @@ -0,0 +1,92 @@ +/* + * 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 { Role } from './role'; +import { User } from './user'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { Browser } from '../../../functional/services/browser'; +import { TestSubjects } from '../../../functional/services/test_subjects'; + +export async function createTestUserService( + role: Role, + user: User, + { getService, hasService }: FtrProviderContext +) { + const log = getService('log'); + const config = getService('config'); + // @ts-ignore browser service is not normally available in common. + const browser: Browser | void = hasService('browser') && getService('browser'); + const testSubjects: TestSubjects | void = + // @ts-ignore testSubject service is not normally available in common. + hasService('testSubjects') && getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + + const enabledPlugins = config.get('security.disableTestUser') + ? [] + : await kibanaServer.plugins.getEnabledIds(); + const isEnabled = () => { + return enabledPlugins.includes('security') && !config.get('security.disableTestUser'); + }; + if (isEnabled()) { + log.debug('===============creating roles and users==============='); + for (const [name, definition] of Object.entries(config.get('security.roles'))) { + // create the defined roles (need to map array to create roles) + await role.create(name, definition); + } + try { + // delete the test_user if present (will it error if the user doesn't exist?) + await user.delete('test_user'); + } catch (exception) { + log.debug('no test user to delete'); + } + + // create test_user with username and pwd + log.debug(`default roles = ${config.get('security.defaultRoles')}`); + await user.create('test_user', { + password: 'changeme', + roles: config.get('security.defaultRoles'), + full_name: 'test user', + }); + } + + return new (class TestUser { + async restoreDefaults() { + if (isEnabled()) { + await this.setRoles(config.get('security.defaultRoles')); + } + } + + async setRoles(roles: string[]) { + if (isEnabled()) { + log.debug(`set roles = ${roles}`); + await user.create('test_user', { + password: 'changeme', + roles, + full_name: 'test user', + }); + + if (browser && testSubjects) { + if (await testSubjects.exists('kibanaChrome', { allowHidden: true })) { + await browser.refresh(); + await testSubjects.find('kibanaChrome', config.get('timeouts.find') * 10); + } + } + } + } + })(); +} diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index d4acdb0b4d5c0..bd132e3745caa 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -26,11 +26,13 @@ const TEST_STEP_SIZE = 3; export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const docTable = getService('docTable'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); describe('context view for date_nanos', () => { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await esArchiver.loadIfNeeded('date_nanos'); await kibanaServer.uiSettings.replace({ defaultIndex: TEST_INDEX_PATTERN }); await kibanaServer.uiSettings.update({ @@ -39,8 +41,9 @@ export default function({ getService, getPageObjects }) { }); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos'); + after(async function unloadMakelogs() { + await security.testUser.restoreDefaults(); + await esArchiver.unload('date_nanos'); }); it('displays predessors - anchor - successors in right order ', async function() { diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index 046cca0aba8c6..7834b29931a65 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -26,12 +26,14 @@ const TEST_STEP_SIZE = 3; export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const docTable = getService('docTable'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); // skipped due to a recent change in ES that caused search_after queries with data containing // custom timestamp formats like in the testdata to fail describe.skip('context view for date_nanos with custom timestamp', () => { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_custom']); await esArchiver.loadIfNeeded('date_nanos_custom'); await kibanaServer.uiSettings.replace({ defaultIndex: TEST_INDEX_PATTERN }); await kibanaServer.uiSettings.update({ @@ -40,10 +42,6 @@ export default function({ getService, getPageObjects }) { }); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos_custom'); - }); - it('displays predessors - anchor - successors in right order ', async function() { await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, '1'); const actualRowsText = await docTable.getRowsText(); @@ -54,5 +52,10 @@ export default function({ getService, getPageObjects }) { ]; expect(actualRowsText).to.eql(expectedRowsText); }); + + after(async function() { + await security.testUser.restoreDefaults(); + await esArchiver.unload('date_nanos_custom'); + }); }); } diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index ec8a48ca74911..f388993dcaf7d 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -33,6 +33,7 @@ export default function({ getService, getPageObjects }) { const filterBar = getService('filterBar'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); @@ -41,6 +42,7 @@ export default function({ getService, getPageObjects }) { before(async () => { await esArchiver.load('dashboard/current/kibana'); + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']); await kibanaServer.uiSettings.replace({ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', }); @@ -49,6 +51,10 @@ export default function({ getService, getPageObjects }) { await PageObjects.dashboard.gotoDashboardLandingPage(); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + describe('adding a filter that excludes all data', () => { before(async () => { await PageObjects.dashboard.clickNewDashboard(); diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index 13e8631445393..5e96a55b19014 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -23,6 +23,7 @@ export default function({ getService, loadTestFile }) { async function loadCurrentData() { await browser.setWindowSize(1300, 900); + await esArchiver.unload('logstash_functional'); await esArchiver.loadIfNeeded('dashboard/current/data'); } diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js index f374d6526fcf1..b7698a7d6ac4b 100644 --- a/test/functional/apps/dashboard/time_zones.js +++ b/test/functional/apps/dashboard/time_zones.js @@ -22,7 +22,6 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const pieChart = getService('pieChart'); - const browser = getService('browser'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['dashboard', 'timePicker', 'settings', 'common']); @@ -48,7 +47,6 @@ export default function({ getService, getPageObjects }) { after(async () => { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC' }); - await browser.refresh(); }); it('Exported dashboard adjusts EST time to UTC', async () => { diff --git a/test/functional/apps/discover/_date_nanos.js b/test/functional/apps/discover/_date_nanos.js index 9b06b9ac84cfd..99a37cc18feaa 100644 --- a/test/functional/apps/discover/_date_nanos.js +++ b/test/functional/apps/discover/_date_nanos.js @@ -23,6 +23,7 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const fromTime = 'Sep 22, 2019 @ 20:31:44.000'; const toTime = 'Sep 23, 2019 @ 03:31:44.000'; @@ -30,12 +31,14 @@ export default function({ getService, getPageObjects }) { before(async function() { await esArchiver.loadIfNeeded('date_nanos'); await kibanaServer.uiSettings.replace({ defaultIndex: 'date-nanos' }); + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos'); + after(async function unloadMakelogs() { + await security.testUser.restoreDefaults(); + await esArchiver.unload('date_nanos'); }); it('should show a timestamp with nanoseconds in the first result row', async function() { diff --git a/test/functional/apps/discover/_date_nanos_mixed.js b/test/functional/apps/discover/_date_nanos_mixed.js index 0bb6848db4d10..b88ae87601cc5 100644 --- a/test/functional/apps/discover/_date_nanos_mixed.js +++ b/test/functional/apps/discover/_date_nanos_mixed.js @@ -23,6 +23,7 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const fromTime = 'Jan 1, 2019 @ 00:00:00.000'; const toTime = 'Jan 1, 2019 @ 23:59:59.999'; @@ -30,12 +31,14 @@ export default function({ getService, getPageObjects }) { before(async function() { await esArchiver.loadIfNeeded('date_nanos_mixed'); await kibanaServer.uiSettings.replace({ defaultIndex: 'timestamp-*' }); + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_mixed']); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos_mixed'); + after(async () => { + await security.testUser.restoreDefaults(); + esArchiver.unload('date_nanos_mixed'); }); it('shows a list of records of indices with date & date_nanos fields in the right order', async function() { diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js index 9310838666256..f815c505a8c27 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.js @@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }) { const browser = getService('browser'); const elasticChart = getService('elasticChart'); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const PageObjects = getPageObjects(['settings', 'common', 'discover', 'header', 'timePicker']); const defaultSettings = { defaultIndex: 'long-window-logstash-*', @@ -35,6 +36,11 @@ export default function({ getService, getPageObjects }) { before(async function() { log.debug('load kibana index with default index pattern'); await PageObjects.common.navigateToApp('home'); + await security.testUser.setRoles([ + 'kibana_admin', + 'test_logstash_reader', + 'long_window_logstash', + ]); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('long_window_logstash'); await esArchiver.load('visualize'); @@ -56,6 +62,7 @@ export default function({ getService, getPageObjects }) { await esArchiver.unload('long_window_logstash'); await esArchiver.unload('visualize'); await esArchiver.unload('discover'); + await security.testUser.restoreDefaults(); }); it('should visualize monthly data with different day intervals', async () => { diff --git a/test/functional/apps/discover/_large_string.js b/test/functional/apps/discover/_large_string.js index a5052b2403074..5e9048e2bc481 100644 --- a/test/functional/apps/discover/_large_string.js +++ b/test/functional/apps/discover/_large_string.js @@ -25,10 +25,12 @@ export default function({ getService, getPageObjects }) { const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover']); describe('test large strings', function() { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'kibana_large_strings']); await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('hamlet'); await kibanaServer.uiSettings.replace({ defaultIndex: 'testlargestring' }); @@ -77,6 +79,7 @@ export default function({ getService, getPageObjects }) { }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('hamlet'); }); }); diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index 5af1676cf423f..ded4eca908410 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -23,6 +23,7 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); + const security = getService('security'); const PageObjects = getPageObjects([ 'console', 'common', @@ -46,11 +47,16 @@ export default function({ getService, getPageObjects }) { 'Load empty_kibana and Shakespeare Getting Started data\n' + 'https://www.elastic.co/guide/en/kibana/current/tutorial-load-dataset.html' ); + await security.testUser.setRoles(['kibana_admin', 'test_shakespeare_reader']); await esArchiver.load('empty_kibana', { skipExisting: true }); log.debug('Load shakespeare data'); await esArchiver.loadIfNeeded('getting_started/shakespeare'); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should create shakespeare index pattern', async function() { log.debug('Create shakespeare index pattern'); await PageObjects.settings.createIndexPattern('shakes', null); diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 8bc528e045566..5812b9b96e42a 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const find = getService('find'); const log = getService('log'); + const security = getService('security'); const pieChart = getService('pieChart'); const renderable = getService('renderable'); const dashboardExpect = getService('dashboardExpect'); @@ -34,10 +35,15 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { this.tags('smoke'); before(async () => { + await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData'); await PageObjects.header.waitUntilLoadingHasFinished(); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should display registered flights sample data sets', async () => { await retry.try(async () => { const exists = await PageObjects.home.doesSampleDataSetExist('flights'); diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js index 55f6b56d9f0d1..4ef02f6c9e873 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.js @@ -23,11 +23,13 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); const retry = getService('retry'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'timePicker']); // FLAKY: https://github.com/elastic/kibana/issues/59717 describe.skip('Index patterns on aliases', function() { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'test_alias_reader']); await esArchiver.loadIfNeeded('alias'); await esArchiver.load('empty_kibana'); await es.indices.updateAliases({ @@ -84,6 +86,7 @@ export default function({ getService, getPageObjects }) { }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('alias'); }); }); diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index 643cbcbe89482..bc280e51ae048 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -21,6 +21,7 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings']); describe('test large number of fields', function() { @@ -28,6 +29,7 @@ export default function({ getService, getPageObjects }) { const EXPECTED_FIELD_COUNT = '10006'; before(async function() { + await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); await esArchiver.loadIfNeeded('large_fields'); await PageObjects.settings.createIndexPattern('testhuge', 'date'); }); @@ -38,6 +40,7 @@ export default function({ getService, getPageObjects }) { }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('large_fields'); }); }); diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index 101b2d4f547dd..bf836cfe778b4 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -24,6 +24,7 @@ export default function({ getService, getPageObjects }) { const inspector = getService('inspector'); const browser = getService('browser'); const retry = getService('retry'); + const security = getService('security'); const PageObjects = getPageObjects([ 'common', 'visualize', @@ -58,7 +59,14 @@ export default function({ getService, getPageObjects }) { return PageObjects.visEditor.clickGo(); }; - before(initAreaChart); + before(async function() { + await security.testUser.setRoles([ + 'kibana_admin', + 'long_window_logstash', + 'test_logstash_reader', + ]); + await initAreaChart(); + }); it('should save and load with special characters', async function() { const vizNamewithSpecialChars = vizName1 + '/?&=%'; @@ -284,6 +292,7 @@ export default function({ getService, getPageObjects }) { .pop() .replace('embed=true', ''); await PageObjects.common.navigateToUrl('visualize', embedUrl); + await security.testUser.restoreDefaults(); }); }); diff --git a/test/functional/apps/visualize/_experimental_vis.js b/test/functional/apps/visualize/_experimental_vis.js index 2ce15cf913eff..c45a95abab86e 100644 --- a/test/functional/apps/visualize/_experimental_vis.js +++ b/test/functional/apps/visualize/_experimental_vis.js @@ -23,7 +23,7 @@ export default ({ getService, getPageObjects }) => { const log = getService('log'); const PageObjects = getPageObjects(['visualize']); - describe('visualize app', function() { + describe('experimental visualizations in visualize app ', function() { this.tags('smoke'); describe('experimental visualizations', () => { diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/_linked_saved_searches.ts index 345987a803394..ea42f7c671985 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/_linked_saved_searches.ts @@ -32,7 +32,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { 'visChart', ]); - describe('visualize app', function describeIndexTests() { + describe('saved search visualizations from visualize app', function describeIndexTests() { describe('linked saved searched', () => { const savedSearchName = 'vis_saved_search'; diff --git a/test/functional/apps/visualize/_markdown_vis.js b/test/functional/apps/visualize/_markdown_vis.js index fee6c074af5d2..649fe0a8e4c2e 100644 --- a/test/functional/apps/visualize/_markdown_vis.js +++ b/test/functional/apps/visualize/_markdown_vis.js @@ -29,7 +29,7 @@ export default function({ getPageObjects, getService }) {

Inline HTML that should not be rendered as html

`; - describe('visualize app', () => { + describe('markdown app in visualize app', () => { before(async function() { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 6a4bed3ba5892..867db66ac81dc 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -25,11 +25,13 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const log = getService('log'); const inspector = getService('inspector'); + const security = getService('security'); const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker', 'visChart']); describe('visual builder', function describeIndexTests() { this.tags('smoke'); beforeEach(async () => { + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisualBuilder(); await PageObjects.visualBuilder.checkVisualBuilderIsPresent(); @@ -111,8 +113,10 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualBuilder.resetPage(); await PageObjects.visualBuilder.clickMetric(); await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('kibana_sample_data_flights'); }); diff --git a/test/functional/apps/visualize/_vega_chart.js b/test/functional/apps/visualize/_vega_chart.js index df0603c7f95f5..7a19bde341cdd 100644 --- a/test/functional/apps/visualize/_vega_chart.js +++ b/test/functional/apps/visualize/_vega_chart.js @@ -25,7 +25,7 @@ export default function({ getService, getPageObjects }) { const inspector = getService('inspector'); const log = getService('log'); - describe('visualize app', () => { + describe('vega chart in visualize app', () => { before(async () => { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index f48ba7b54daf1..8f079f5cc430d 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -25,10 +25,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const find = getService('find'); + const security = getService('security'); const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); describe('input control range', () => { before(async () => { + await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await esArchiver.load('kibana_sample_data_flights_index_pattern'); await visualize.navigateToNewVisualization(); await visualize.clickInputControlVis(); @@ -63,6 +65,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.loadIfNeeded('long_window_logstash'); await esArchiver.load('visualize'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await security.testUser.restoreDefaults(); }); }); } diff --git a/test/functional/config.js b/test/functional/config.js index e84b7e0a98a68..11399bd6187c8 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -103,5 +103,172 @@ export default async function({ readConfigFile }) { browser: { type: 'chrome', }, + + security: { + roles: { + test_logstash_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['logstash*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + test_shakespeare_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['shakes*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + test_testhuge_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['testhuge*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + test_alias_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['alias*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + //for sample data - can remove but not add sample data.( not ml)- for ml use built in role. + kibana_sample_admin: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['kibana_sample*'], + privileges: ['read', 'view_index_metadata', 'manage', 'create_index', 'index'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_date_nanos: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['date-nanos'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_date_nanos_custom: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['date_nanos_custom_timestamp'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_date_nanos_mixed: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['date_nanos_mixed', 'timestamp-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_large_strings: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['testlargestring'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + long_window_logstash: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['long-window-logstash-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + animals: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['animals-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + }, + defaultRoles: ['test_logstash_reader', 'kibana_admin'], + }, }; } diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 60966511c1f99..5ee3726ddb44f 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -105,13 +105,16 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo const wantedLoginPage = appUrl.includes('/login') || appUrl.includes('/logout'); if (loginPage && !wantedLoginPage) { - log.debug( - `Found login page. Logging in with username = ${config.get('servers.kibana.username')}` - ); - await PageObjects.shield.login( - config.get('servers.kibana.username'), - config.get('servers.kibana.password') - ); + log.debug('Found login page'); + if (config.get('security.disableTestUser')) { + await PageObjects.shield.login( + config.get('servers.kibana.username'), + config.get('servers.kibana.password') + ); + } else { + await PageObjects.shield.login('test_user', 'changeme'); + } + await find.byCssSelector( '[data-test-subj="kibanaChrome"] nav:not(.ng-hide)', 6 * defaultFindTimeout diff --git a/test/functional/screenshots/baseline/area_chart.png b/test/functional/screenshots/baseline/area_chart.png index 2c2d599139100..1a381d61dd9f1 100644 Binary files a/test/functional/screenshots/baseline/area_chart.png and b/test/functional/screenshots/baseline/area_chart.png differ diff --git a/test/functional/screenshots/baseline/tsvb_dashboard.png b/test/functional/screenshots/baseline/tsvb_dashboard.png index d703be89b7460..f5ebccbcb96c6 100644 Binary files a/test/functional/screenshots/baseline/tsvb_dashboard.png and b/test/functional/screenshots/baseline/tsvb_dashboard.png differ diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 02349b4e6cca2..5017947e95d03 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -21,6 +21,7 @@ import { cloneDeep } from 'lodash'; import { Key, Origin } from 'selenium-webdriver'; // @ts-ignore internal modules are not typed import { LegacyActionSequence } from 'selenium-webdriver/lib/actions'; +import { ProvidedType } from '@kbn/test/types/ftr'; import Jimp from 'jimp'; import { modifyUrl } from '../../../src/core/utils'; @@ -28,6 +29,7 @@ import { WebElementWrapper } from './lib/web_element_wrapper'; import { FtrProviderContext } from '../ftr_provider_context'; import { Browsers } from './remote/browsers'; +export type Browser = ProvidedType; export async function BrowserProvider({ getService }: FtrProviderContext) { const log = getService('log'); const { driver, browserType } = await getService('__webdriver__').init(); diff --git a/test/functional/services/test_subjects.ts b/test/functional/services/test_subjects.ts index d47b838c8d72a..e5c2e61c48a0b 100644 --- a/test/functional/services/test_subjects.ts +++ b/test/functional/services/test_subjects.ts @@ -19,6 +19,7 @@ import testSubjSelector from '@kbn/test-subj-selector'; import { map as mapAsync } from 'bluebird'; +import { ProvidedType } from '@kbn/test/types/ftr'; import { WebElementWrapper } from './lib/web_element_wrapper'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -32,6 +33,7 @@ interface SetValueOptions { typeCharByChar?: boolean; } +export type TestSubjects = ProvidedType; export function TestSubjectsProvider({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); diff --git a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts index fad19728b7514..3f6a8e8773e04 100644 --- a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts +++ b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts @@ -33,7 +33,7 @@ export class RenderingPlugin implements Plugin { { includeUserSettings: schema.boolean({ defaultValue: true }), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), params: schema.object({ id: schema.maybe(schema.string()), diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 60a8d1fcbf229..1564eb94a6903 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -22,7 +22,7 @@ "xpack.infra": "plugins/infra", "xpack.ingestManager": "plugins/ingest_manager", "xpack.lens": "legacy/plugins/lens", - "xpack.licenseMgmt": "legacy/plugins/license_management", + "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", "xpack.logstash": "legacy/plugins/logstash", "xpack.main": "legacy/plugins/xpack_main", diff --git a/x-pack/index.js b/x-pack/index.js index ab31d40c5d718..fb14b3dc10a4d 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -16,7 +16,6 @@ import { logstash } from './legacy/plugins/logstash'; import { beats } from './legacy/plugins/beats_management'; import { apm } from './legacy/plugins/apm'; import { maps } from './legacy/plugins/maps'; -import { licenseManagement } from './legacy/plugins/license_management'; import { indexManagement } from './legacy/plugins/index_management'; import { indexLifecycleManagement } from './legacy/plugins/index_lifecycle_management'; import { spaces } from './legacy/plugins/spaces'; @@ -52,7 +51,6 @@ module.exports = function(kibana) { apm(kibana), maps(kibana), canvas(kibana), - licenseManagement(kibana), indexManagement(kibana), indexLifecycleManagement(kibana), infra(kibana), diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx index 31fc4db8f1a2f..cff190cd98a11 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx @@ -209,7 +209,7 @@ export function MachineLearningFlyoutView({ {i18n.translate( 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel', { - defaultMessage: 'Create new job' + defaultMessage: 'Create job' } )} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx new file mode 100644 index 0000000000000..d61dea80666a0 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, render, wait } from '@testing-library/react'; +import cytoscape from 'cytoscape'; +import React, { FunctionComponent } from 'react'; +import { MockApmPluginContextWrapper } from '../../../utils/testHelpers'; +import { CytoscapeContext } from './Cytoscape'; +import { EmptyBanner } from './EmptyBanner'; + +const cy = cytoscape({}); + +const wrapper: FunctionComponent = ({ children }) => ( + + {children} + +); + +describe('EmptyBanner', () => { + describe('when cy is undefined', () => { + it('renders null', () => { + const noCytoscapeWrapper: FunctionComponent = ({ children }) => ( + + + {children} + + + ); + const component = render(, { + wrapper: noCytoscapeWrapper + }); + + expect(component.container.children).toHaveLength(0); + }); + }); + + describe('with no nodes', () => { + it('renders null', () => { + const component = render(, { + wrapper + }); + + expect(component.container.children).toHaveLength(0); + }); + }); + + describe('with one node', () => { + it('does not render null', async () => { + const component = render(, { wrapper }); + + await act(async () => { + cy.add({ data: { id: 'test id' } }); + await wait(() => { + expect(component.container.children.length).toBeGreaterThan(0); + }); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx index 418430e37b21e..464bf166eb80f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx @@ -7,37 +7,70 @@ import { EuiCallOut } from '@elastic/eui'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; +import { CytoscapeContext } from './Cytoscape'; -const EmptyBannerCallOut = styled(EuiCallOut)` +const EmptyBannerContainer = styled.div` margin: ${lightTheme.gutterTypes.gutterSmall}; /* Add some extra margin so it displays to the right of the controls. */ - margin-left: calc( - ${lightTheme.gutterTypes.gutterLarge} + - ${lightTheme.gutterTypes.gutterExtraLarge} + left: calc( + ${lightTheme.gutterTypes.gutterExtraLarge} + + ${lightTheme.gutterTypes.gutterSmall} ); position: absolute; z-index: 1; `; export function EmptyBanner() { + const cy = useContext(CytoscapeContext); + const [nodeCount, setNodeCount] = useState(0); + + useEffect(() => { + const handler: cytoscape.EventHandler = event => + setNodeCount(event.cy.nodes().length); + + if (cy) { + cy.on('add remove', 'node', handler); + } + + return () => { + if (cy) { + cy.removeListener('add remove', 'node', handler); + } + }; + }, [cy]); + + // Only show if there's a single node. + if (!cy || nodeCount !== 1) { + return null; + } + + // Since we're absolutely positioned, we need to get the full width and + // subtract the space for controls and margins. + const width = + cy.width() - + parseInt(lightTheme.gutterTypes.gutterExtraLarge, 10) - + parseInt(lightTheme.gutterTypes.gutterLarge, 10); + return ( - - {i18n.translate('xpack.apm.serviceMap.emptyBanner.message', { - defaultMessage: - "We will map out connected services and external requests if we can detect them. Please make sure you're running the latest version of the APM agent." - })}{' '} - - {i18n.translate('xpack.apm.serviceMap.emptyBanner.docsLink', { - defaultMessage: 'Learn more in the docs' + + - + > + {i18n.translate('xpack.apm.serviceMap.emptyBanner.message', { + defaultMessage: + "We will map out connected services and external requests if we can detect them. Please make sure you're running the latest version of the APM agent." + })}{' '} + + {i18n.translate('xpack.apm.serviceMap.emptyBanner.docsLink', { + defaultMessage: 'Learn more in the docs' + })} + +
+ ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx index 9213349a1492b..77f0b64ba0fb1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx @@ -6,10 +6,12 @@ import { EuiButton, - EuiEmptyPrompt, + EuiPanel, EuiFlexGroup, EuiFlexItem, - EuiPanel + EuiTitle, + EuiText, + EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; @@ -18,7 +20,8 @@ import { useKibanaUrl } from '../../../hooks/useKibanaUrl'; export function PlatinumLicensePrompt() { // Set the height to give it some top margin - const style = { height: '60vh' }; + const flexGroupStyle = { height: '60vh' }; + const flexItemStyle = { width: 600, textAlign: 'center' as const }; const licensePageUrl = useKibanaUrl( '/app/kibana', @@ -29,30 +32,41 @@ export function PlatinumLicensePrompt() { - - - - {i18n.translate( - 'xpack.apm.serviceMap.licensePromptButtonText', - { - defaultMessage: 'Start 30-day Platinum trial' - } - )} - - ]} - body={

{invalidLicenseMessage}

} - title={ + + + + +

{i18n.translate('xpack.apm.serviceMap.licensePromptTitle', { defaultMessage: 'Service maps is available in Platinum.' })}

- } - /> +
+ + +

{invalidLicenseMessage}

+
+ + + {i18n.translate('xpack.apm.serviceMap.licensePromptButtonText', { + defaultMessage: 'Start 30-day Platinum trial' + })} + +
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg index 9f7427f0e1001..da7f1a8fde45d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg @@ -1,127 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index 2942ce64729e7..93aa3d406028c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { ElementDefinition } from 'cytoscape'; @@ -16,7 +15,8 @@ import React, { useRef, useState } from 'react'; -import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; +import { EuiBetaBadge } from '@elastic/eui'; +import styled from 'styled-components'; import { isValidPlatinumLicense } from '../../../../../../../plugins/apm/common/service_map'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceMapAPIResponse } from '../../../../../../../plugins/apm/server/lib/service_map/get_service_map'; @@ -58,7 +58,12 @@ ${theme.euiColorLightShade}`, margin: `-${theme.gutterTypes.gutterLarge}`, marginTop: 0 }; - +const BetaBadgeContainer = styled.div` + right: ${theme.gutterTypes.gutterMedium}; + position: absolute; + top: ${theme.gutterTypes.gutterSmall}; + z-index: 1; /* The element containing the cytoscape canvas has z-index = 0. */ +`; const MAX_REQUESTS = 5; export function ServiceMap({ serviceName }: ServiceMapProps) { @@ -78,7 +83,6 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { }); const renderedElements = useRef([]); - const openToast = useRef(null); const [responses, setResponses] = useState([]); @@ -160,41 +164,11 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return !find(renderedElements.current, el => isEqual(el, element)); }); - const updateMap = () => { + if (newElements.length > 0 && renderedElements.current.length > 0) { renderedElements.current = elements; - if (openToast.current) { - notifications.toasts.remove(openToast.current); - } forceUpdate(); - }; - - if (newElements.length > 0 && renderedElements.current.length > 0) { - openToast.current = notifications.toasts.add({ - title: i18n.translate('xpack.apm.newServiceMapData', { - defaultMessage: `Newly discovered connections are available.` - }), - onClose: () => { - openToast.current = null; - }, - toastLifeTimeMs: 24 * 60 * 60 * 1000, - text: toMountPoint( - - {i18n.translate('xpack.apm.updateServiceMap', { - defaultMessage: 'Update map' - })} - - ) - }).id; } - - return () => { - if (openToast.current) { - notifications.toasts.remove(openToast.current); - } - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [elements]); + }, [elements, forceUpdate]); const { ref: wrapperRef, width, height } = useRefDimensions(); @@ -215,10 +189,22 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { style={cytoscapeDivStyle} > - {serviceName && renderedElements.current.length === 1 && ( - - )} + {serviceName && } + + + ) : ( diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts index 7f81adad6bf9b..949264fcc9fdb 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; -import * as jobCompletionNotifications from '../../../../../reporting/public/lib/job_completion_notifications'; +import { jobCompletionNotifications } from '../../../../../../../plugins/reporting/public'; // @ts-ignore Untyped local import { getWorkpad, getPages } from '../../../state/selectors/workpad'; // @ts-ignore Untyped local diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js index ebb731a1b1aca..d4e418a964c8f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js @@ -29,7 +29,8 @@ import { EuiTitle, } from '@elastic/eui'; -import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; +import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../../src/plugins/data/public'; import routing from '../services/routing'; import { extractQueryParams } from '../services/query_params'; @@ -44,10 +45,9 @@ import { } from '../services/auto_follow_pattern_validators'; import { AutoFollowPatternRequestFlyout } from './auto_follow_pattern_request_flyout'; -import { indexPatterns } from '../../../../../../../src/plugins/data/public'; const indexPatternIllegalCharacters = indexPatterns.ILLEGAL_CHARACTERS_VISIBLE.join(' '); -const indexNameIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); +const indexNameIllegalCharacters = indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); const getEmptyAutoFollowPattern = (remoteClusterName = '') => ({ name: '', diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js index 329ef4756133d..fc8f9398807e7 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js @@ -9,7 +9,6 @@ import PropTypes from 'prop-types'; import { debounce } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; import { fatalError } from 'ui/notify'; import { @@ -30,6 +29,7 @@ import { EuiTitle, } from '@elastic/eui'; +import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; import { indexNameValidator, leaderIndexValidator } from '../../services/input_validation'; import routing from '../../services/routing'; import { loadIndices } from '../../services/api'; @@ -47,7 +47,7 @@ import { RemoteClustersFormField } from '../remote_clusters_form_field'; import { FollowerIndexRequestFlyout } from './follower_index_request_flyout'; -const indexNameIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); +const indexNameIllegalCharacters = indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); const fieldToValidatorMap = advancedSettingsFields.reduce( (map, advancedSetting) => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js index aac0427098813..93da20a8ed93c 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js @@ -7,9 +7,6 @@ import { updateFields, updateFormErrors } from './follower_index_form'; jest.mock('ui/new_platform'); -jest.mock('ui/indices', () => ({ - INDEX_ILLEGAL_CHARACTERS_VISIBLE: [], -})); describe(' state transitions', () => { it('updateFormErrors() should merge errors with existing fieldsErrors', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js index 18610c87c0a51..5186a02383d33 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js @@ -8,13 +8,14 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { +import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../../src/plugins/data/public'; + +const { indexNameBeginsWithPeriod, findIllegalCharactersInIndexName, indexNameContainsSpaces, -} from 'ui/indices'; - -import { indexPatterns } from '../../../../../../../src/plugins/data/public'; +} = indices; export const validateName = (name = '') => { let errorMsg = null; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js index 22f7d3be2795f..981b3f5929751 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; +import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; const isEmpty = value => { return !value || !value.trim().length; @@ -19,7 +19,7 @@ const beginsWithPeriod = value => { }; const findIllegalCharacters = value => { - return INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => { + return indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => { if (value.includes(char)) { chars.push(char); } diff --git a/x-pack/legacy/plugins/lens/public/_config_panel.scss b/x-pack/legacy/plugins/lens/public/_config_panel.scss deleted file mode 100644 index 5c6d25bf10818..0000000000000 --- a/x-pack/legacy/plugins/lens/public/_config_panel.scss +++ /dev/null @@ -1,21 +0,0 @@ -.lnsConfigPanel__panel { - margin-bottom: $euiSizeS; -} - -.lnsConfigPanel__axis { - background: $euiColorLightestShade; - padding: $euiSizeS; - border-radius: $euiBorderRadius; - - // Add margin to the top of the next same panel - & + & { - margin-top: $euiSizeS; - } -} - -.lnsConfigPanel__addLayerBtn { - color: transparentize($euiColorMediumShade, .3); - // sass-lint:disable-block no-important - box-shadow: none !important; - border: 1px dashed currentColor; -} diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx index 0cba22170df1f..e18190b6c2d69 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -4,18 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { createMockDatasource } from '../editor_frame_service/mocks'; -import { - DatatableVisualizationState, - datatableVisualization, - DataTableLayer, -} from './visualization'; -import { mount } from 'enzyme'; +import { DatatableVisualizationState, datatableVisualization } from './visualization'; import { Operation, DataType, FramePublicAPI, TableSuggestionColumn } from '../types'; -import { generateId } from '../id_generator'; - -jest.mock('../id_generator'); function mockFrame(): FramePublicAPI { return { @@ -34,12 +25,11 @@ function mockFrame(): FramePublicAPI { describe('Datatable Visualization', () => { describe('#initialize', () => { it('should initialize from the empty state', () => { - (generateId as jest.Mock).mockReturnValueOnce('id'); expect(datatableVisualization.initialize(mockFrame(), undefined)).toEqual({ layers: [ { layerId: 'aaa', - columns: ['id'], + columns: [], }, ], }); @@ -88,7 +78,6 @@ describe('Datatable Visualization', () => { describe('#clearLayer', () => { it('should reset the layer', () => { - (generateId as jest.Mock).mockReturnValueOnce('testid'); const state: DatatableVisualizationState = { layers: [ { @@ -101,7 +90,7 @@ describe('Datatable Visualization', () => { layers: [ { layerId: 'baz', - columns: ['testid'], + columns: [], }, ], }); @@ -214,29 +203,35 @@ describe('Datatable Visualization', () => { }); }); - describe('DataTableLayer', () => { - it('allows all kinds of operations', () => { - const setState = jest.fn(); - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; + describe('#getConfiguration', () => { + it('returns a single layer option', () => { + const datasource = createMockDatasource('test'); const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; + frame.datasourceLayers = { first: datasource.publicAPIMock }; - mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); + expect( + datatableVisualization.getConfiguration({ + layerId: 'first', + state: { + layers: [{ layerId: 'first', columns: [] }], + }, + frame, + }).groups + ).toHaveLength(1); + }); - expect(datasource.publicAPIMock.renderDimensionPanel).toHaveBeenCalled(); + it('allows all kinds of operations', () => { + const datasource = createMockDatasource('test'); + const frame = mockFrame(); + frame.datasourceLayers = { first: datasource.publicAPIMock }; - const filterOperations = - datasource.publicAPIMock.renderDimensionPanel.mock.calls[0][1].filterOperations; + const filterOperations = datatableVisualization.getConfiguration({ + layerId: 'first', + state: { + layers: [{ layerId: 'first', columns: [] }], + }, + frame, + }).groups[0].filterOperations; const baseOperation: Operation = { dataType: 'string', @@ -253,108 +248,80 @@ describe('Datatable Visualization', () => { ); }); - it('allows columns to be removed', () => { - const setState = jest.fn(); - const datasource = createMockDatasource(); + it('reorders the rendered colums based on the order from the datasource', () => { + const datasource = createMockDatasource('test'); const layer = { layerId: 'a', columns: ['b', 'c'] }; const frame = mockFrame(); frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); - - const onRemove = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('onRemove') as (k: string) => {}; - - onRemove('b'); + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + + expect( + datatableVisualization.getConfiguration({ + layerId: 'a', + state: { layers: [layer] }, + frame, + }).groups[0].accessors + ).toEqual(['c', 'b']); + }); + }); - expect(setState).toHaveBeenCalledWith({ + describe('#removeDimension', () => { + it('allows columns to be removed', () => { + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.removeDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'b', + }) + ).toEqual({ layers: [ { - layerId: 'a', + layerId: 'layer1', columns: ['c'], }, ], }); }); + }); + describe('#setDimension', () => { it('allows columns to be added', () => { - (generateId as jest.Mock).mockReturnValueOnce('d'); - const setState = jest.fn(); - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; - const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); - - const onAdd = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('onAdd') as () => {}; - - onAdd(); - - expect(setState).toHaveBeenCalledWith({ + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.setDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'd', + groupId: '', + }) + ).toEqual({ layers: [ { - layerId: 'a', + layerId: 'layer1', columns: ['b', 'c', 'd'], }, ], }); }); - it('reorders the rendered colums based on the order from the datasource', () => { - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; - const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={jest.fn()} - state={{ layers: [layer] }} - /> - ); - - const accessors = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('accessors') as string[]; - - expect(accessors).toEqual(['b', 'c']); - - component.setProps({ - layer: { layerId: 'a', columns: ['c', 'b'] }, + it('does not set a duplicate dimension', () => { + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.setDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'b', + groupId: '', + }) + ).toEqual({ + layers: [ + { + layerId: 'layer1', + columns: ['b', 'c'], + }, + ], }); - - const newAccessors = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('accessors') as string[]; - - expect(newAccessors).toEqual(['c', 'b']); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx index 79a018635134f..4248d722d5540 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx @@ -4,20 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { render } from 'react-dom'; -import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; -import { MultiColumnEditor } from '../multi_column_editor'; -import { - SuggestionRequest, - Visualization, - VisualizationLayerConfigProps, - VisualizationSuggestion, - Operation, -} from '../types'; -import { generateId } from '../id_generator'; +import { SuggestionRequest, Visualization, VisualizationSuggestion, Operation } from '../types'; import chartTableSVG from '../assets/chart_datatable.svg'; export interface LayerState { @@ -32,58 +20,10 @@ export interface DatatableVisualizationState { function newLayerState(layerId: string): LayerState { return { layerId, - columns: [generateId()], + columns: [], }; } -function updateColumns( - state: DatatableVisualizationState, - layer: LayerState, - fn: (columns: string[]) => string[] -) { - const columns = fn(layer.columns); - const updatedLayer = { ...layer, columns }; - const layers = state.layers.map(l => (l.layerId === layer.layerId ? updatedLayer : l)); - return { ...state, layers }; -} - -const allOperations = () => true; - -export function DataTableLayer({ - layer, - frame, - state, - setState, - dragDropContext, -}: { layer: LayerState } & VisualizationLayerConfigProps) { - const datasource = frame.datasourceLayers[layer.layerId]; - - const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); - // When we add a column it could be empty, and therefore have no order - const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); - - return ( - - setState(updateColumns(state, layer, columns => [...columns, generateId()]))} - onRemove={column => - setState(updateColumns(state, layer, columns => columns.filter(c => c !== column))) - } - testSubj="datatable_columns" - data-test-subj="datatable_multicolumnEditor" - /> - - ); -} - export const datatableVisualization: Visualization< DatatableVisualizationState, DatatableVisualizationState @@ -188,17 +128,56 @@ export const datatableVisualization: Visualization< ]; }, - renderLayerConfigPanel(domElement, props) { - const layer = props.state.layers.find(l => l.layerId === props.layerId); - - if (layer) { - render( - - - , - domElement - ); + getConfiguration({ state, frame, layerId }) { + const layer = state.layers.find(l => l.layerId === layerId); + if (!layer) { + return { groups: [] }; } + + const datasource = frame.datasourceLayers[layer.layerId]; + const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); + // When we add a column it could be empty, and therefore have no order + const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); + + return { + groups: [ + { + groupId: 'columns', + groupLabel: i18n.translate('xpack.lens.datatable.columns', { + defaultMessage: 'Columns', + }), + layerId: state.layers[0].layerId, + accessors: sortedColumns, + supportsMoreColumns: true, + filterOperations: () => true, + }, + ], + }; + }, + + setDimension({ prevState, layerId, columnId }) { + return { + ...prevState, + layers: prevState.layers.map(l => { + if (l.layerId !== layerId || l.columns.includes(columnId)) { + return l; + } + return { ...l, columns: [...l.columns, columnId] }; + }), + }; + }, + removeDimension({ prevState, layerId, columnId }) { + return { + ...prevState, + layers: prevState.layers.map(l => + l.layerId === layerId + ? { + ...l, + columns: l.columns.filter(c => c !== columnId), + } + : l + ), + }; }, toExpression(state, frame) { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss new file mode 100644 index 0000000000000..62a7f6b023f31 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss @@ -0,0 +1,50 @@ +.lnsConfigPanel__panel { + margin-bottom: $euiSizeS; +} + +.lnsConfigPanel__row { + background: $euiColorLightestShade; + padding: $euiSizeS; + border-radius: $euiBorderRadius; + + // Add margin to the top of the next same panel + & + & { + margin-top: $euiSizeS; + } +} + +.lnsConfigPanel__addLayerBtn { + color: transparentize($euiColorMediumShade, .3); + // Remove EuiButton's default shadow to make button more subtle + // sass-lint:disable-block no-important + box-shadow: none !important; + border: 1px dashed currentColor; +} + +.lnsConfigPanel__dimension { + @include euiFontSizeS; + background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); + border-radius: $euiBorderRadius; + display: flex; + align-items: center; + margin-top: $euiSizeXS; + overflow: hidden; +} + +.lnsConfigPanel__trigger { + max-width: 100%; + display: block; +} + +.lnsConfigPanel__triggerLink { + padding: $euiSizeS; + width: 100%; + display: flex; + align-items: center; + min-height: $euiSizeXXL; +} + +.lnsConfigPanel__popover { + line-height: 0; + flex-grow: 1; +} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx index 1b60098fd45ad..6698c9e68b98c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx @@ -84,7 +84,7 @@ describe('chart_switch', () => { } function mockDatasourceMap() { - const datasource = createMockDatasource(); + const datasource = createMockDatasource('testDatasource'); datasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ { state: {}, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 1422ee86be3e9..c2cd0485de67e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -16,17 +16,21 @@ import { EuiToolTip, EuiButton, EuiForm, + EuiFormRow, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; import { Visualization, FramePublicAPI, Datasource, - VisualizationLayerConfigProps, + VisualizationLayerWidgetProps, + DatasourceDimensionEditorProps, + StateSetter, } from '../../types'; -import { DragContext } from '../../drag_drop'; +import { DragContext, DragDrop, ChildDragDropProvider } from '../../drag_drop'; import { ChartSwitch } from './chart_switch'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { generateId } from '../../id_generator'; @@ -47,6 +51,7 @@ interface ConfigPanelWrapperProps { state: unknown; } >; + core: DatasourceDimensionEditorProps['core']; } export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { @@ -86,8 +91,7 @@ function LayerPanels( activeDatasourceId, datasourceMap, } = props; - const dragDropContext = useContext(DragContext); - const setState = useMemo( + const setVisualizationState = useMemo( () => (newState: unknown) => { props.dispatch({ type: 'UPDATE_VISUALIZATION_STATE', @@ -98,6 +102,43 @@ function LayerPanels( }, [props.dispatch, activeVisualization] ); + const updateDatasource = useMemo( + () => (datasourceId: string, newState: unknown) => { + props.dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: () => newState, + datasourceId, + clearStagedPreview: false, + }); + }, + [props.dispatch] + ); + const updateAll = useMemo( + () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { + props.dispatch({ + type: 'UPDATE_STATE', + subType: 'UPDATE_ALL_STATES', + updater: prevState => { + return { + ...prevState, + datasourceStates: { + ...prevState.datasourceStates, + [datasourceId]: { + state: newDatasourceState, + isLoading: false, + }, + }, + visualization: { + activeId: activeVisualization.id, + state: newVisualizationState, + }, + stagedPreview: undefined, + }; + }, + }); + }, + [props.dispatch] + ); const layerIds = activeVisualization.getLayerIds(visualizationState); return ( @@ -108,12 +149,13 @@ function LayerPanels( key={layerId} layerId={layerId} activeVisualization={activeVisualization} - dragDropContext={dragDropContext} - state={setState} - setState={setState} + visualizationState={visualizationState} + updateVisualization={setVisualizationState} + updateDatasource={updateDatasource} + updateAll={updateAll} frame={framePublicAPI} isOnlyLayer={layerIds.length === 1} - onRemove={() => { + onRemoveLayer={() => { dispatch({ type: 'UPDATE_STATE', subType: 'REMOVE_OR_CLEAR_LAYER', @@ -143,7 +185,7 @@ function LayerPanels( className="lnsConfigPanel__addLayerBtn" fullWidth size="s" - data-test-subj={`lnsXY_layer_add`} + data-test-subj="lnsXY_layer_add" aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', { defaultMessage: 'Add layer', })} @@ -174,85 +216,399 @@ function LayerPanels( } function LayerPanel( - props: ConfigPanelWrapperProps & - VisualizationLayerConfigProps & { - isOnlyLayer: boolean; - activeVisualization: Visualization; - onRemove: () => void; - } + props: Exclude & { + frame: FramePublicAPI; + layerId: string; + isOnlyLayer: boolean; + activeVisualization: Visualization; + visualizationState: unknown; + updateVisualization: StateSetter; + updateDatasource: (datasourceId: string, newState: unknown) => void; + updateAll: ( + datasourceId: string, + newDatasourcestate: unknown, + newVisualizationState: unknown + ) => void; + onRemoveLayer: () => void; + } ) { - const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemove } = props; + const dragDropContext = useContext(DragContext); + const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - const layerConfigProps = { + if (!datasourcePublicAPI) { + return null; + } + const layerVisualizationConfigProps = { layerId, - dragDropContext: props.dragDropContext, + dragDropContext, state: props.visualizationState, - setState: props.setState, frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, }; + const datasourceId = datasourcePublicAPI.datasourceId; + const layerDatasourceState = props.datasourceStates[datasourceId].state; + const layerDatasource = props.datasourceMap[datasourceId]; - return ( - - - - - + const layerDatasourceDropProps = { + layerId, + dragDropContext, + state: layerDatasourceState, + setState: (newState: unknown) => { + props.updateDatasource(datasourceId, newState); + }, + }; - {datasourcePublicAPI && ( - - ({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + + const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); + const isEmptyLayer = !groups.some(d => d.accessors.length > 0); + + function wrapInPopover( + id: string, + groupId: string, + trigger: React.ReactElement, + panel: React.ReactElement + ) { + const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(id)) : false; + return ( + { + setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); + }} + button={trigger} + anchorPosition="leftUp" + withTitle + panelPaddingSize="s" + > + {panel} + + ); + } + + return ( + + + + + - )} - - - - + {layerDatasource && ( + + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + dateRange: props.framePublicAPI.dateRange, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter(columnId => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach(columnId => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + }); + }); - + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + + )} + - - - { - // If we don't blur the remove / clear button, it remains focused - // which is a strange UX in this case. e.target.blur doesn't work - // due to who knows what, but probably event re-writing. Additionally, - // activeElement does not have blur so, we need to do some casting + safeguards. - const el = (document.activeElement as unknown) as { blur: () => void }; + - if (el && el.blur) { - el.blur(); + {groups.map((group, index) => { + const newId = generateId(); + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + + <> + {group.accessors.map(accessor => ( + { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: group.filterOperations, + }); + }} + > + {wrapInPopover( + accessor, + group.groupId, + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: accessor, + addingToGroupId: null, // not set for existing dimension + }); + } + }, + }} + />, + + )} - onRemove(); - }} - > - {isOnlyLayer - ? i18n.translate('xpack.lens.resetLayer', { - defaultMessage: 'Reset layer', - }) - : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: 'Delete layer', - })} - - - - + { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: accessor, + prevState: layerDatasourceState, + }), + props.activeVisualization.removeDimension({ + layerId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> + + ))} + {group.supportsMoreColumns ? ( + { + const dropSuccess = layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: newId, + filterOperations: group.filterOperations, + }); + if (dropSuccess) { + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + } + }} + > + {wrapInPopover( + newId, + group.groupId, +
+ { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: group.groupId, + }); + } + }} + size="xs" + > + + +
, + { + props.updateAll( + datasourceId, + newState, + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: null, // clear now that dimension exists + }); + }, + }} + /> + )} +
+ ) : null} + + + ); + })} + + + + + + { + // If we don't blur the remove / clear button, it remains focused + // which is a strange UX in this case. e.target.blur doesn't work + // due to who knows what, but probably event re-writing. Additionally, + // activeElement does not have blur so, we need to do some casting + safeguards. + const el = (document.activeElement as unknown) as { blur: () => void }; + + if (el?.blur) { + el.blur(); + } + + onRemoveLayer(); + }} + > + {isOnlyLayer + ? i18n.translate('xpack.lens.resetLayer', { + defaultMessage: 'Reset layer', + }) + : i18n.translate('xpack.lens.deleteLayer', { + defaultMessage: 'Delete layer', + })} + + + + + ); } @@ -263,7 +619,7 @@ function LayerSettings({ }: { layerId: string; activeVisualization: Visualization; - layerConfigProps: VisualizationLayerConfigProps; + layerConfigProps: VisualizationLayerWidgetProps; }) { const [isOpen, setIsOpen] = useState(false); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index dd591b3992fe5..8d8d38944e18a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -87,14 +87,15 @@ describe('editor_frame', () => { mockVisualization.getLayerIds.mockReturnValue(['first']); mockVisualization2.getLayerIds.mockReturnValue(['second']); - mockDatasource = createMockDatasource(); - mockDatasource2 = createMockDatasource(); + mockDatasource = createMockDatasource('testDatasource'); + mockDatasource2 = createMockDatasource('testDatasource2'); expressionRendererMock = createExpressionRendererMock(); }); describe('initialization', () => { it('should initialize initial datasource', async () => { + mockVisualization.getLayerIds.mockReturnValue([]); await act(async () => { mount( { }); it('should initialize all datasources with state from doc', async () => { - const mockDatasource3 = createMockDatasource(); + const mockDatasource3 = createMockDatasource('testDatasource3'); const datasource1State = { datasource1: '' }; const datasource2State = { datasource2: '' }; @@ -198,9 +199,9 @@ describe('editor_frame', () => { ExpressionRenderer={expressionRendererMock} /> ); - expect(mockVisualization.renderLayerConfigPanel).not.toHaveBeenCalled(); expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled(); }); + expect(mockDatasource.renderDataPanel).toHaveBeenCalled(); }); it('should not initialize visualization before datasource is initialized', async () => { @@ -289,6 +290,7 @@ describe('editor_frame', () => { mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState)); mockDatasource2.getLayers.mockReturnValue(['abc', 'def']); mockDatasource2.removeLayer.mockReturnValue({ removed: true }); + mockVisualization.getLayerIds.mockReturnValue(['first', 'abc', 'def']); await act(async () => { mount( { ); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: initialState }) ); }); @@ -614,15 +615,14 @@ describe('editor_frame', () => { ); }); const updatedState = {}; - const setVisualizationState = (mockVisualization.renderLayerConfigPanel as jest.Mock).mock - .calls[0][1].setState; + const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] + .setState; act(() => { - setVisualizationState(updatedState); + setDatasourceState(updatedState); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); + expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ state: updatedState, }) @@ -688,8 +688,7 @@ describe('editor_frame', () => { }); const updatedPublicAPI: DatasourcePublicAPI = { - renderLayerPanel: jest.fn(), - renderDimensionPanel: jest.fn(), + datasourceId: 'testDatasource', getOperationForColumnId: jest.fn(), getTableSpec: jest.fn(), }; @@ -701,9 +700,8 @@ describe('editor_frame', () => { setDatasourceState({}); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); + expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ frame: expect.objectContaining({ datasourceLayers: { @@ -719,6 +717,7 @@ describe('editor_frame', () => { it('should pass the datasource api for each layer to the visualization', async () => { mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource2.getLayers.mockReturnValue(['second', 'third']); + mockVisualization.getLayerIds.mockReturnValue(['first', 'second', 'third']); await act(async () => { mount( @@ -755,10 +754,10 @@ describe('editor_frame', () => { ); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalled(); + expect(mockVisualization.getConfiguration).toHaveBeenCalled(); const datasourceLayers = - mockVisualization.renderLayerConfigPanel.mock.calls[0][1].frame.datasourceLayers; + mockVisualization.getConfiguration.mock.calls[0][0].frame.datasourceLayers; expect(datasourceLayers.first).toBe(mockDatasource.publicAPIMock); expect(datasourceLayers.second).toBe(mockDatasource2.publicAPIMock); expect(datasourceLayers.third).toBe(mockDatasource2.publicAPIMock); @@ -811,21 +810,18 @@ describe('editor_frame', () => { expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource1State, - setState: expect.anything(), layerId: 'first', }) ); expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource2State, - setState: expect.anything(), layerId: 'second', }) ); expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource2State, - setState: expect.anything(), layerId: 'third', }) ); @@ -858,45 +854,9 @@ describe('editor_frame', () => { expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ dateRange, state: datasourceState, - setState: expect.any(Function), layerId: 'first', }); }); - - it('should re-create the public api after state has been set', async () => { - mockDatasource.getLayers.mockReturnValue(['first']); - - await act(async () => { - mount( - - ); - }); - - const updatedState = {}; - const setDatasourceState = mockDatasource.getPublicAPI.mock.calls[0][0].setState; - act(() => { - setDatasourceState(updatedState); - }); - - expect(mockDatasource.getPublicAPI).toHaveBeenLastCalledWith( - expect.objectContaining({ - state: updatedState, - setState: expect.any(Function), - layerId: 'first', - }) - ); - }); }); describe('switching', () => { @@ -1021,8 +981,7 @@ describe('editor_frame', () => { expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); expect(mockVisualization2.initialize).toHaveBeenCalledWith(expect.anything(), initialState); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: { initial: true } }) ); }); @@ -1039,8 +998,7 @@ describe('editor_frame', () => { datasourceLayers: expect.objectContaining({ first: mockDatasource.publicAPIMock }), }) ); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: { initial: true } }) ); }); @@ -1239,9 +1197,8 @@ describe('editor_frame', () => { .simulate('click'); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(1); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(1); + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1306,8 +1263,7 @@ describe('editor_frame', () => { .simulate('drop'); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1375,14 +1331,16 @@ describe('editor_frame', () => { instance.update(); act(() => { - instance.find(DragDrop).prop('onDrop')!({ + instance + .find(DragDrop) + .filter('[data-test-subj="mockVisA"]') + .prop('onDrop')!({ indexPatternId: '1', field: {}, }); }); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1472,14 +1430,16 @@ describe('editor_frame', () => { instance.update(); act(() => { - instance.find(DragDrop).prop('onDrop')!({ + instance + .find(DragDrop) + .filter('[data-test-subj="lnsWorkspace"]') + .prop('onDrop')!({ indexPatternId: '1', field: {}, }); }); - expect(mockVisualization3.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization3.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index a456372c99c01..082519d9a8feb 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -21,6 +21,7 @@ import { FrameLayout } from './frame_layout'; import { SuggestionPanel } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; import { Document } from '../../persistence/saved_object_store'; +import { RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; @@ -90,21 +91,11 @@ export function EditorFrame(props: EditorFrameProps) { const layers = datasource.getLayers(datasourceState); layers.forEach(layer => { - const publicAPI = props.datasourceMap[id].getPublicAPI({ + datasourceLayers[layer] = props.datasourceMap[id].getPublicAPI({ state: datasourceState, - setState: (newState: unknown) => { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - datasourceId: id, - updater: newState, - clearStagedPreview: true, - }); - }, layerId: layer, dateRange: props.dateRange, }); - - datasourceLayers[layer] = publicAPI; }); }); @@ -235,74 +226,79 @@ export function EditorFrame(props: EditorFrameProps) { ]); return ( - - } - configPanel={ - allLoaded && ( - + - ) - } - workspacePanel={ - allLoaded && ( - - + ) + } + workspacePanel={ + allLoaded && ( + + + + ) + } + suggestionsPanel={ + allLoaded && ( + - - ) - } - suggestionsPanel={ - allLoaded && ( - - ) - } - /> + ) + } + /> + ); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx index a69da8b49e233..56afe3ed69a73 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { EuiPage, EuiPageSideBar, EuiPageBody } from '@elastic/eui'; -import { RootDragDropProvider } from '../../drag_drop'; export interface FrameLayoutProps { dataPanel: React.ReactNode; @@ -17,19 +16,17 @@ export interface FrameLayoutProps { export function FrameLayout(props: FrameLayoutProps) { return ( - - -
- {props.dataPanel} - - {props.workspacePanel} - {props.suggestionsPanel} - - - {props.configPanel} - -
-
-
+ +
+ {props.dataPanel} + + {props.workspacePanel} + {props.suggestionsPanel} + + + {props.configPanel} + +
+
); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss index fee28c374ef7e..6c6a63c8c7eb6 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss @@ -1,4 +1,5 @@ @import './chart_switch'; +@import './config_panel_wrapper'; @import './data_panel_wrapper'; @import './expression_renderer'; @import './frame_layout'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts index 158a6cb8c979a..60bfbc493f61c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts @@ -11,7 +11,7 @@ import { esFilters, IIndexPattern, IFieldType } from '../../../../../../../src/p describe('save editor frame state', () => { const mockVisualization = createMockVisualization(); mockVisualization.getPersistableState.mockImplementation(x => x); - const mockDatasource = createMockDatasource(); + const mockDatasource = createMockDatasource('a'); const mockIndexPattern = ({ id: 'indexpattern' } as unknown) as IIndexPattern; const mockField = ({ name: '@timestamp' } as unknown) as IFieldType; @@ -45,7 +45,7 @@ describe('save editor frame state', () => { }; it('transforms from internal state to persisted doc format', async () => { - const datasource = createMockDatasource(); + const datasource = createMockDatasource('a'); datasource.getPersistableState.mockImplementation(state => ({ stuff: `${state}_datasource_persisted`, })); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index 487a91c22b5d5..63b8b1f048296 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -30,7 +30,7 @@ let datasourceStates: Record< beforeEach(() => { datasourceMap = { - mock: createMockDatasource(), + mock: createMockDatasource('a'), }; datasourceStates = { @@ -147,9 +147,9 @@ describe('suggestion helpers', () => { }, }; const multiDatasourceMap = { - mock: createMockDatasource(), - mock2: createMockDatasource(), - mock3: createMockDatasource(), + mock: createMockDatasource('a'), + mock2: createMockDatasource('a'), + mock3: createMockDatasource('a'), }; const droppedField = {}; getSuggestions({ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index 9729d6259f84a..b146f2467c46c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -39,7 +39,7 @@ describe('suggestion_panel', () => { beforeEach(() => { mockVisualization = createMockVisualization(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('a'); expressionRendererMock = createExpressionRendererMock(); dispatchMock = jest.fn(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 1115126792c86..93f6ea6ea67ac 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -373,7 +373,6 @@ function getPreviewExpression( layerId, dateRange: frame.dateRange, state: datasourceState, - setState: () => {}, }); } }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx index a51091d39f84c..748e5b876da95 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx @@ -36,7 +36,7 @@ describe('workspace_panel', () => { mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('a'); expressionRendererMock = createExpressionRendererMock(); }); @@ -199,7 +199,7 @@ describe('workspace_panel', () => { }); it('should include data fetching for each layer in the expression', () => { - const mockDatasource2 = createMockDatasource(); + const mockDatasource2 = createMockDatasource('a'); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index e606c69c8c386..5d2f68a5567eb 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -33,9 +33,24 @@ export function createMockVisualization(): jest.Mocked { getPersistableState: jest.fn(_state => _state), getSuggestions: jest.fn(_options => []), initialize: jest.fn((_frame, _state?) => ({})), - renderLayerConfigPanel: jest.fn(), + getConfiguration: jest.fn(props => ({ + groups: [ + { + groupId: 'a', + groupLabel: 'a', + layerId: 'layer1', + supportsMoreColumns: true, + accessors: [], + filterOperations: jest.fn(() => true), + dataTestSubj: 'mockVisA', + }, + ], + })), toExpression: jest.fn((_state, _frame) => null), toPreviewExpression: jest.fn((_state, _frame) => null), + + setDimension: jest.fn(), + removeDimension: jest.fn(), }; } @@ -43,12 +58,11 @@ export type DatasourceMock = jest.Mocked & { publicAPIMock: jest.Mocked; }; -export function createMockDatasource(): DatasourceMock { +export function createMockDatasource(id: string): DatasourceMock { const publicAPIMock: jest.Mocked = { + datasourceId: id, getTableSpec: jest.fn(() => []), getOperationForColumnId: jest.fn(), - renderDimensionPanel: jest.fn(), - renderLayerPanel: jest.fn(), }; return { @@ -60,12 +74,19 @@ export function createMockDatasource(): DatasourceMock { getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), + renderLayerPanel: jest.fn(), toExpression: jest.fn((_frame, _state) => null), insertLayer: jest.fn((_state, _newLayerId) => {}), removeLayer: jest.fn((_state, _layerId) => {}), + removeColumn: jest.fn(props => {}), getLayers: jest.fn(_state => []), getMetaData: jest.fn(_state => ({ filterableIndexPatterns: [] })), + renderDimensionTrigger: jest.fn(), + renderDimensionEditor: jest.fn(), + canHandleDrop: jest.fn(), + onDrop: jest.fn(), + // this is an additional property which doesn't exist on real datasources // but can be used to validate whether specific API mock functions are called publicAPIMock, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index 6b9dc88e7ed12..47fd810bb4c53 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -47,7 +47,7 @@ describe('editor_frame service', () => { pluginSetupDependencies ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance({}); + const instance = await publicAPI.createInstance(); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), @@ -66,7 +66,7 @@ describe('editor_frame service', () => { pluginSetupDependencies ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance({}); + const instance = await publicAPI.createInstance(); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index 496573f6a1c9a..2f91d14c397c7 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -4,8 +4,6 @@ @import './variables'; @import './mixins'; -@import './config_panel'; - @import './app_plugin/index'; @import 'datatable_visualization/index'; @import './drag_drop/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss deleted file mode 100644 index ddb37505f9985..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss +++ /dev/null @@ -1,9 +0,0 @@ -.lnsIndexPatternDimensionPanel { - @include euiFontSizeS; - background-color: $euiColorEmptyShade; - border-radius: $euiBorderRadius; - display: flex; - align-items: center; - margin-top: $euiSizeXS; - overflow: hidden; -} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss index 2ce3e11171fc9..26f805fe735f0 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss @@ -1,3 +1,2 @@ -@import './dimension_panel'; @import './field_select'; @import './popover'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss index 8f26ab91e0f16..07a72ee1f66fc 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss @@ -1,37 +1,24 @@ -.lnsPopoverEditor { +.lnsIndexPatternDimensionEditor { flex-grow: 1; line-height: 0; overflow: hidden; } -.lnsPopoverEditor__anchor { - max-width: 100%; - display: block; -} - -.lnsPopoverEditor__link { - width: 100%; - display: flex; - align-items: center; - padding: $euiSizeS; - min-height: $euiSizeXXL; -} - -.lnsPopoverEditor__left, -.lnsPopoverEditor__right { +.lnsIndexPatternDimensionEditor__left, +.lnsIndexPatternDimensionEditor__right { padding: $euiSizeS; } -.lnsPopoverEditor__left { +.lnsIndexPatternDimensionEditor__left { padding-top: 0; background-color: $euiPageBackgroundColor; } -.lnsPopoverEditor__right { +.lnsIndexPatternDimensionEditor__right { width: $euiSize * 20; } -.lnsPopoverEditor__operation { +.lnsIndexPatternDimensionEditor__operation { @include euiFontSizeS; color: $euiColorPrimary; @@ -41,11 +28,11 @@ } } -.lnsPopoverEditor__operation--selected { +.lnsIndexPatternDimensionEditor__operation--selected { font-weight: bold; color: $euiTextColor; } -.lnsPopoverEditor__operation--incompatible { +.lnsIndexPatternDimensionEditor__operation--incompatible { color: $euiColorMediumShade; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 56f75ae4b17be..41c317ccab290 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -7,27 +7,28 @@ import { ReactWrapper, ShallowWrapper } from 'enzyme'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import { - EuiComboBox, - EuiSideNav, - EuiSideNavItemType, - EuiPopover, - EuiFieldNumber, -} from '@elastic/eui'; +import { EuiComboBox, EuiSideNav, EuiSideNavItemType, EuiFieldNumber } from '@elastic/eui'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { changeColumn } from '../state_helpers'; import { - IndexPatternDimensionPanel, - IndexPatternDimensionPanelComponent, - IndexPatternDimensionPanelProps, + IndexPatternDimensionEditorComponent, + IndexPatternDimensionEditorProps, + onDrop, + canHandleDrop, } from './dimension_panel'; -import { DropHandler, DragContextState } from '../../drag_drop'; +import { DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; +import { + IUiSettingsClient, + SavedObjectsClientContract, + HttpSetup, + CoreSetup, +} from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; +import { OperationMetadata } from '../../types'; jest.mock('ui/new_platform'); jest.mock('../loader'); @@ -79,20 +80,12 @@ const expectedIndexPatterns = { }, }; -describe('IndexPatternDimensionPanel', () => { - let wrapper: ReactWrapper | ShallowWrapper; +describe('IndexPatternDimensionEditorPanel', () => { let state: IndexPatternPrivateState; let setState: jest.Mock; - let defaultProps: IndexPatternDimensionPanelProps; + let defaultProps: IndexPatternDimensionEditorProps; let dragDropContext: DragContextState; - function openPopover() { - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .simulate('click'); - } - beforeEach(() => { state = { indexPatternRefs: [], @@ -134,7 +127,6 @@ describe('IndexPatternDimensionPanel', () => { dragDropContext = createMockedDragDropContext(); defaultProps = { - dragDropContext, state, setState, dateRange: { fromDate: 'now-1d', toDate: 'now' }, @@ -158,475 +150,582 @@ describe('IndexPatternDimensionPanel', () => { }), } as unknown) as DataPublicPluginStart['fieldFormats'], } as unknown) as DataPublicPluginStart, + core: {} as CoreSetup, }; jest.clearAllMocks(); }); - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - } - }); + describe('Editor component', () => { + let wrapper: ReactWrapper | ShallowWrapper; - it('should display a configure button if dimension has no column yet', () => { - wrapper = mount(); - expect( - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .prop('iconType') - ).toEqual('plusInCircleFilled'); - }); + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); - it('should call the filterOperations function', () => { - const filterOperations = jest.fn().mockReturnValue(true); + it('should call the filterOperations function', () => { + const filterOperations = jest.fn().mockReturnValue(true); - wrapper = shallow( - - ); + wrapper = shallow( + + ); - expect(filterOperations).toBeCalled(); - }); + expect(filterOperations).toBeCalled(); + }); - it('should show field select combo box on click', () => { - wrapper = mount(); + it('should show field select combo box on click', () => { + wrapper = mount(); - openPopover(); + expect( + wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') + ).toHaveLength(1); + }); - expect( - wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') - ).toHaveLength(1); - }); + it('should not show any choices if the filter returns false', () => { + wrapper = mount( + false} + /> + ); - it('should not show any choices if the filter returns false', () => { - wrapper = mount( - false} - /> - ); + expect( + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')! + .prop('options')! + ).toHaveLength(0); + }); - openPopover(); + it('should list all field names and document as a whole in prioritized order', () => { + wrapper = mount(); - expect( - wrapper + const options = wrapper .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')! - .prop('options')! - ).toHaveLength(0); - }); - - it('should list all field names and document as a whole in prioritized order', () => { - wrapper = mount(); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - expect(options).toHaveLength(2); + expect(options).toHaveLength(2); - expect(options![0].label).toEqual('Records'); + expect(options![0].label).toEqual('Records'); - expect(options![1].options!.map(({ label }) => label)).toEqual([ - 'timestamp', - 'bytes', - 'memory', - 'source', - ]); - }); + expect(options![1].options!.map(({ label }) => label)).toEqual([ + 'timestamp', + 'bytes', + 'memory', + 'source', + ]); + }); - it('should hide fields that have no data', () => { - const props = { - ...defaultProps, - state: { - ...defaultProps.state, - existingFields: { - 'my-fake-index-pattern': { - timestamp: true, - source: true, + it('should hide fields that have no data', () => { + const props = { + ...defaultProps, + state: { + ...defaultProps.state, + existingFields: { + 'my-fake-index-pattern': { + timestamp: true, + source: true, + }, }, }, - }, - }; - wrapper = mount(); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + }; + wrapper = mount(); - expect(options![1].options!.map(({ label }) => label)).toEqual(['timestamp', 'source']); - }); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - it('should indicate fields which are incompatible for the operation of the current column', () => { - wrapper = mount( - label)).toEqual(['timestamp', 'source']); + }); - // Private - operationType: 'max', - sourceField: 'bytes', + it('should indicate fields which are incompatible for the operation of the current column', () => { + wrapper = mount( + - ); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + }} + /> + ); - expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); + expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); - it('should indicate operations which are incompatible for the field of the current column', () => { - wrapper = mount( - label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - // Private - operationType: 'max', - sourceField: 'bytes', + it('should indicate operations which are incompatible for the field of the current column', () => { + wrapper = mount( + - ); - - openPopover(); + }} + /> + ); - interface ItemType { - name: string; - 'data-test-subj': string; - } - const items: Array> = wrapper.find(EuiSideNav).prop('items'); - const options = (items[0].items as unknown) as ItemType[]; + interface ItemType { + name: string; + 'data-test-subj': string; + } + const items: Array> = wrapper.find(EuiSideNav).prop('items'); + const options = (items[0].items as unknown) as ItemType[]; - expect(options.find(({ name }) => name === 'Minimum')!['data-test-subj']).not.toContain( - 'Incompatible' - ); + expect(options.find(({ name }) => name === 'Minimum')!['data-test-subj']).not.toContain( + 'Incompatible' + ); - expect(options.find(({ name }) => name === 'Date histogram')!['data-test-subj']).toContain( - 'Incompatible' - ); - }); + expect(options.find(({ name }) => name === 'Date histogram')!['data-test-subj']).toContain( + 'Incompatible' + ); + }); - it('should keep the operation when switching to another field compatible with this operation', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, + it('should keep the operation when switching to another field compatible with this operation', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, - // Private - operationType: 'max', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, + // Private + operationType: 'max', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; - act(() => { - comboBox.prop('onChange')!([option]); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'max', - sourceField: 'memory', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...initialState, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'max', + sourceField: 'memory', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - - it('should switch operations when selecting a field that requires another operation', () => { - wrapper = mount(); - openPopover(); + it('should switch operations when selecting a field that requires another operation', () => { + wrapper = mount(); - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; - act(() => { - comboBox.prop('onChange')!([option]); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'terms', - sourceField: 'source', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - - it('should keep the field when switching to another operation compatible for this field', () => { - wrapper = mount( - { + wrapper = mount( + - ); - - openPopover(); + }} + /> + ); - act(() => { - wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); - }); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'min', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - it('should not set the state if selecting the currently active operation', () => { - wrapper = mount(); + it('should not set the state if selecting the currently active operation', () => { + wrapper = mount(); - openPopover(); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + }); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); + expect(setState).not.toHaveBeenCalled(); }); - expect(setState).not.toHaveBeenCalled(); - }); - - it('should update label on label input changes', () => { - wrapper = mount(); + it('should update label on label input changes', () => { + wrapper = mount(); - openPopover(); - - act(() => { - wrapper - .find('input[data-test-subj="indexPattern-label-edit"]') - .simulate('change', { target: { value: 'New Label' } }); - }); + act(() => { + wrapper + .find('input[data-test-subj="indexPattern-label-edit"]') + .simulate('change', { target: { value: 'New Label' } }); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'New Label', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'New Label', + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - describe('transient invalid state', () => { - it('should not set the state if selecting an operation incompatible with the current field', () => { - wrapper = mount(); + describe('transient invalid state', () => { + it('should not set the state if selecting an operation incompatible with the current field', () => { + wrapper = mount(); - openPopover(); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); + }); + + expect(setState).not.toHaveBeenCalled(); + }); + + it('should show error message in invalid state', () => { + wrapper = mount(); - act(() => { wrapper .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).not.toHaveLength( + 0 + ); + + expect(setState).not.toHaveBeenCalled(); }); - expect(setState).not.toHaveBeenCalled(); - }); + it('should leave error state if a compatible operation is selected', () => { + wrapper = mount(); - it('should show error message in invalid state', () => { - wrapper = mount(); + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - openPopover(); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).not.toHaveLength(0); + it('should indicate fields compatible with selected operation', () => { + wrapper = mount(); - expect(setState).not.toHaveBeenCalled(); - }); + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - it('should leave error state if a compatible operation is selected', () => { - wrapper = mount(); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - openPopover(); + expect(options![0]['data-test-subj']).toContain('Incompatible'); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); + it('should select compatible operation if field not compatible with selected operation', () => { + wrapper = mount( + + ); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - it('should leave error state if the popover gets closed', () => { - wrapper = mount(); + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]'); + const options = comboBox.prop('options'); - openPopover(); + // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation + act(() => { + comboBox.prop('onChange')!([options![1].options![2]]); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, + }, + }); + }); - act(() => { - wrapper.find(EuiPopover).prop('closePopover')!(); + it('should select the Records field when count is selected', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'avg', + sourceField: 'bytes', + }, + }, + }, + }, + }; + wrapper = mount( + + ); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-count"]') + .simulate('click'); + + const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; + expect(newColumnState.operationType).toEqual('count'); + expect(newColumnState.sourceField).toEqual('Records'); }); - openPopover(); + it('should indicate document and field compatibility with selected document operation', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'count', + sourceField: 'Records', + }, + }, + }, + }, + }; + wrapper = mount( + + ); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - it('should indicate fields compatible with selected operation', () => { - wrapper = mount(); + expect(options![0]['data-test-subj']).toContain('Incompatible'); - openPopover(); + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + it('should set datasource state if compatible field is selected for operation', () => { + wrapper = mount(); - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); + }); - expect(options![0]['data-test-subj']).toContain('Incompatible'); + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox + .prop('options')![1] + .options!.find(({ label }) => label === 'source')!; - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - it('should select compatible operation if field not compatible with selected operation', () => { - wrapper = mount(); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + }), + }, + }, + }, + }); + }); + }); - openPopover(); + it('should support selecting the operation before the field', () => { + wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); @@ -635,9 +734,8 @@ describe('IndexPatternDimensionPanel', () => { .filter('[data-test-subj="indexPattern-dimension-field"]'); const options = comboBox.prop('options'); - // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation act(() => { - comboBox.prop('onChange')!([options![1].options![2]]); + comboBox.prop('onChange')!([options![1].options![0]]); }); expect(setState).toHaveBeenCalledWith({ @@ -648,8 +746,8 @@ describe('IndexPatternDimensionPanel', () => { columns: { ...state.layers.first.columns, col2: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', + sourceField: 'bytes', + operationType: 'avg', // Other parts of this don't matter for this test }), }, @@ -659,41 +757,93 @@ describe('IndexPatternDimensionPanel', () => { }); }); - it('should select the Records field when count is selected', () => { - const initialState: IndexPatternPrivateState = { + it('should select operation directly if only one field is possible', () => { + const initialState = { + ...state, + indexPatterns: { + 1: { + ...state.indexPatterns['1'], + fields: state.indexPatterns['1'].fields.filter(field => field.name !== 'memory'), + }, + }, + }; + + wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + + expect(setState).toHaveBeenCalledWith({ + ...initialState, + layers: { + first: { + ...initialState.layers.first, + columns: { + ...initialState.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, + }, + }); + }); + + it('should select operation directly if only document is possible', () => { + wrapper = mount(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + + expect(setState).toHaveBeenCalledWith({ ...state, layers: { first: { ...state.layers.first, columns: { ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'avg', - sourceField: 'bytes', - }, + col2: expect.objectContaining({ + operationType: 'count', + // Other parts of this don't matter for this test + }), }, + columnOrder: ['col1', 'col2'], }, }, - }; - wrapper = mount( - - ); + }); + }); - openPopover(); + it('should indicate compatible fields when selecting the operation first', () => { + wrapper = mount(); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-count"]') - .simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; - expect(newColumnState.operationType).toEqual('count'); - expect(newColumnState.sourceField).toEqual('Records'); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + + expect(options![0]['data-test-subj']).toContain('Incompatible'); + + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] + ).not.toContain('Incompatible'); }); - it('should indicate document and field compatibility with selected document operation', () => { + it('should indicate document compatibility when document operation is selected', () => { const initialState: IndexPatternPrivateState = { ...state, layers: { @@ -713,45 +863,56 @@ describe('IndexPatternDimensionPanel', () => { }, }; wrapper = mount( - + ); - openPopover(); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); - const options = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]') .prop('options'); - expect(options![0]['data-test-subj']).toContain('Incompatible'); + expect(options![0]['data-test-subj']).not.toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] - ).not.toContain('Incompatible'); + options![1].options!.map(operation => + expect(operation['data-test-subj']).toContain('Incompatible') + ); }); - it('should set datasource state if compatible field is selected for operation', () => { - wrapper = mount(); + it('should show all operations that are not filtered out', () => { + wrapper = mount( + !op.isBucketed && op.dataType === 'number'} + /> + ); - openPopover(); + interface ItemType { + name: React.ReactNode; + } + const items: Array> = wrapper.find(EuiSideNav).prop('items'); + const options = (items[0].items as unknown) as ItemType[]; + + expect(options.map(({ name }: { name: React.ReactNode }) => name)).toEqual([ + 'Unique count', + 'Average', + 'Count', + 'Maximum', + 'Minimum', + 'Sum', + ]); + }); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); - }); + it('should add a column on selection of a field', () => { + wrapper = mount(); const comboBox = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + const option = comboBox.prop('options')![1].options![0]; act(() => { comboBox.prop('onChange')!([option]); @@ -764,479 +925,237 @@ describe('IndexPatternDimensionPanel', () => { ...state.layers.first, columns: { ...state.layers.first.columns, - col1: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', + col2: expect.objectContaining({ + sourceField: 'bytes', + // Other parts of this don't matter for this test }), }, + columnOrder: ['col1', 'col2'], }, }, }); }); - }); - - it('should support selecting the operation before the field', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]'); - const options = comboBox.prop('options'); - - act(() => { - comboBox.prop('onChange')!([options![1].options![0]]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select operation directly if only one field is possible', () => { - const initialState = { - ...state, - indexPatterns: { - 1: { - ...state.indexPatterns['1'], - fields: state.indexPatterns['1'].fields.filter(field => field.name !== 'memory'), - }, - }, - }; - - wrapper = mount( - - ); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...initialState.layers.first, - columns: { - ...initialState.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select operation directly if only document is possible', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - operationType: 'count', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - it('should indicate compatible fields when selecting the operation first', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).toContain('Incompatible'); - - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); - - it('should indicate document compatibility when document operation is selected', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'count', - sourceField: 'Records', - }, - }, - }, - }, - }; - wrapper = mount( - - ); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).not.toContain('Incompatible'); - - options![1].options!.map(operation => - expect(operation['data-test-subj']).toContain('Incompatible') - ); - }); - - it('should show all operations that are not filtered out', () => { - wrapper = mount( - !op.isBucketed && op.dataType === 'number'} - /> - ); - - openPopover(); - - interface ItemType { - name: React.ReactNode; - } - const items: Array> = wrapper.find(EuiSideNav).prop('items'); - const options = (items[0].items as unknown) as ItemType[]; - - expect(options.map(({ name }: { name: React.ReactNode }) => name)).toEqual([ - 'Unique count', - 'Average', - 'Count', - 'Maximum', - 'Minimum', - 'Sum', - ]); - }); - - it('should add a column on selection of a field', () => { - wrapper = mount(); - - openPopover(); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options![0]; - - act(() => { - comboBox.prop('onChange')!([option]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should use helper function when changing the function', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, + it('should use helper function when changing the function', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, - // Private - operationType: 'max', - sourceField: 'bytes', + // Private + operationType: 'max', + sourceField: 'bytes', + }, }, }, }, - }, - }; - wrapper = mount(); - - openPopover(); - - act(() => { - wrapper - .find('[data-test-subj="lns-indexPatternDimension-min"]') - .first() - .prop('onClick')!({} as React.MouseEvent<{}, MouseEvent>); - }); - - expect(changeColumn).toHaveBeenCalledWith({ - state: initialState, - columnId: 'col1', - layerId: 'first', - newColumn: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'min', - }), - }); - }); - - it('should clear the dimension with the clear button', () => { - wrapper = mount(); - - const clearButton = wrapper.find( - 'EuiButtonIcon[data-test-subj="indexPattern-dimensionPopover-remove"]' - ); + }; + wrapper = mount( + + ); - act(() => { - clearButton.simulate('click'); - }); + act(() => { + wrapper + .find('[data-test-subj="lns-indexPatternDimension-min"]') + .first() + .prop('onClick')!({} as React.MouseEvent<{}, MouseEvent>); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], - }, - }, + expect(changeColumn).toHaveBeenCalledWith({ + state: initialState, + columnId: 'col1', + layerId: 'first', + newColumn: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'min', + }), + }); }); - }); - - it('should clear the dimension when removing the selection in field combobox', () => { - wrapper = mount(); - openPopover(); + it('should clear the dimension when removing the selection in field combobox', () => { + wrapper = mount(); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('onChange')!([]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('onChange')!([]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + indexPatternId: '1', + columns: {}, + columnOrder: [], + }, }, - }, + }); }); - }); - it('allows custom format', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', + it('allows custom format', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: 'bytes', label: 'Bytes' }]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: 'bytes', label: 'Bytes' }]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - params: { - format: { id: 'bytes', params: { decimals: 2 } }, - }, - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + params: { + format: { id: 'bytes', params: { decimals: 2 } }, + }, + }), + }, }, }, - }, + }); }); - }); - it('keeps decimal places while switching', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', - params: { - format: { id: 'bytes', params: { decimals: 0 } }, + it('keeps decimal places while switching', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + params: { + format: { id: 'bytes', params: { decimals: 0 } }, + }, }, }, }, }, - }, - }; + }; - wrapper = mount(); + wrapper = mount( + + ); - openPopover(); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: '', label: 'Default' }]); + }); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: '', label: 'Default' }]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: 'number', label: 'Number' }]); + }); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: 'number', label: 'Number' }]); + expect( + wrapper + .find(EuiFieldNumber) + .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .prop('value') + ).toEqual(0); }); - expect( - wrapper - .find(EuiFieldNumber) - .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .prop('value') - ).toEqual(0); - }); - - it('allows custom format with number of decimal places', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', - params: { - format: { id: 'bytes', params: { decimals: 2 } }, + it('allows custom format with number of decimal places', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + params: { + format: { id: 'bytes', params: { decimals: 2 } }, + }, }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - act(() => { - wrapper - .find(EuiFieldNumber) - .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .prop('onChange')!({ target: { value: '0' } }); - }); + act(() => { + wrapper + .find(EuiFieldNumber) + .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .prop('onChange')!({ target: { value: '0' } }); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - params: { - format: { id: 'bytes', params: { decimals: 0 } }, - }, - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + params: { + format: { id: 'bytes', params: { decimals: 0 } }, + }, + }), + }, }, }, - }, + }); }); }); - describe('drag and drop', () => { + describe('Drag and drop', () => { function dragDropState(): IndexPatternPrivateState { return { indexPatternRefs: [], @@ -1287,112 +1206,80 @@ describe('IndexPatternDimensionPanel', () => { } it('is not droppable if no drag is happening', () => { - wrapper = mount( - - ); - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + canHandleDrop({ + ...defaultProps, + dragDropContext, + state: dragDropState(), + layerId: 'myLayer', + }) + ).toBe(false); }); it('is not droppable if the dragged item has no field', () => { - wrapper = shallow( - - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + }) + ).toBe(false); }); it('is not droppable if field is not supported by filterOperations', () => { - wrapper = shallow( - false} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + state: dragDropState(), + filterOperations: () => false, + layerId: 'myLayer', + }) + ).toBe(false); }); it('is droppable if the field is supported by filterOperations', () => { - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeTruthy(); + }, + state: dragDropState(), + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(true); }); - it('is notdroppable if the field belongs to another index pattern', () => { - wrapper = shallow( - { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { ...dragDropContext, dragging: { field: { type: 'number', name: 'bar', aggregatable: true }, indexPatternId: 'foo2', }, - }} - state={dragDropState()} - filterOperations={op => op.dataType === 'number'} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + state: dragDropState(), + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(false); }); it('appends the dropped column when a field is dropped', () => { @@ -1401,27 +1288,18 @@ describe('IndexPatternDimensionPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); @@ -1449,27 +1327,17 @@ describe('IndexPatternDimensionPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.isBucketed} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed, + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); @@ -1497,26 +1365,16 @@ describe('IndexPatternDimensionPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 59350ff215c27..5d87137db3d39 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -5,27 +5,36 @@ */ import _ from 'lodash'; -import React, { memo, useMemo } from 'react'; -import { EuiButtonIcon } from '@elastic/eui'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; +import { EuiLink } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { + DatasourceDimensionTriggerProps, + DatasourceDimensionEditorProps, + DatasourceDimensionDropProps, + DatasourceDimensionDropHandlerProps, +} from '../../types'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; -import { DatasourceDimensionPanelProps, StateSetter } from '../../types'; import { IndexPatternColumn, OperationType } from '../indexpattern'; import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations'; import { PopoverEditor } from './popover_editor'; -import { DragContextState, ChildDragDropProvider, DragDrop } from '../../drag_drop'; -import { changeColumn, deleteColumn } from '../state_helpers'; +import { changeColumn } from '../state_helpers'; import { isDraggedField, hasField } from '../utils'; import { IndexPatternPrivateState, IndexPatternField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { DateRange } from '../../../../../../plugins/lens/common'; -export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { - state: IndexPatternPrivateState; - setState: StateSetter; - dragDropContext: DragContextState; +export type IndexPatternDimensionTriggerProps = DatasourceDimensionTriggerProps< + IndexPatternPrivateState +> & { + uniqueLabel: string; +}; + +export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps< + IndexPatternPrivateState +> & { uiSettings: IUiSettingsClient; storage: IStorageWrapper; savedObjectsClient: SavedObjectsClientContract; @@ -41,152 +50,181 @@ export interface OperationFieldSupportMatrix { fieldByOperation: Partial>; } -export const IndexPatternDimensionPanelComponent = function IndexPatternDimensionPanel( - props: IndexPatternDimensionPanelProps -) { +type Props = Pick< + DatasourceDimensionDropProps, + 'layerId' | 'columnId' | 'state' | 'filterOperations' +>; + +// TODO: This code has historically been memoized, as a potentially performance +// sensitive task. If we can add memoization without breaking the behavior, we should. +const getOperationFieldSupportMatrix = (props: Props): OperationFieldSupportMatrix => { const layerId = props.layerId; const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; - const operationFieldSupportMatrix = useMemo(() => { - const filteredOperationsByMetadata = getAvailableOperationsByMetadata( - currentIndexPattern - ).filter(operation => props.filterOperations(operation.operationMetaData)); - - const supportedOperationsByField: Partial> = {}; - const supportedFieldsByOperation: Partial> = {}; - - filteredOperationsByMetadata.forEach(({ operations }) => { - operations.forEach(operation => { - if (supportedOperationsByField[operation.field]) { - supportedOperationsByField[operation.field]!.push(operation.operationType); - } else { - supportedOperationsByField[operation.field] = [operation.operationType]; - } - - if (supportedFieldsByOperation[operation.operationType]) { - supportedFieldsByOperation[operation.operationType]!.push(operation.field); - } else { - supportedFieldsByOperation[operation.operationType] = [operation.field]; - } - }); + const filteredOperationsByMetadata = getAvailableOperationsByMetadata( + currentIndexPattern + ).filter(operation => props.filterOperations(operation.operationMetaData)); + + const supportedOperationsByField: Partial> = {}; + const supportedFieldsByOperation: Partial> = {}; + + filteredOperationsByMetadata.forEach(({ operations }) => { + operations.forEach(operation => { + if (supportedOperationsByField[operation.field]) { + supportedOperationsByField[operation.field]!.push(operation.operationType); + } else { + supportedOperationsByField[operation.field] = [operation.operationType]; + } + + if (supportedFieldsByOperation[operation.operationType]) { + supportedFieldsByOperation[operation.operationType]!.push(operation.field); + } else { + supportedFieldsByOperation[operation.operationType] = [operation.field]; + } }); - return { - operationByField: _.mapValues(supportedOperationsByField, _.uniq), - fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), - }; - }, [currentIndexPattern, props.filterOperations]); + }); + return { + operationByField: _.mapValues(supportedOperationsByField, _.uniq), + fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), + }; +}; - const selectedColumn: IndexPatternColumn | null = - props.state.layers[layerId].columns[props.columnId] || null; +export function canHandleDrop(props: DatasourceDimensionDropProps) { + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + + const { dragging } = props.dragDropContext; + const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; function hasOperationForField(field: IndexPatternField) { return Boolean(operationFieldSupportMatrix.operationByField[field.name]); } - function canHandleDrop() { - const { dragging } = props.dragDropContext; - const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; + return ( + isDraggedField(dragging) && + layerIndexPatternId === dragging.indexPatternId && + Boolean(hasOperationForField(dragging.field)) + ); +} + +export function onDrop( + props: DatasourceDimensionDropHandlerProps +): boolean { + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + const droppedItem = props.droppedItem; + + function hasOperationForField(field: IndexPatternField) { + return Boolean(operationFieldSupportMatrix.operationByField[field.name]); + } - return ( - isDraggedField(dragging) && - layerIndexPatternId === dragging.indexPatternId && - Boolean(hasOperationForField(dragging.field)) - ); + if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { + // TODO: What do we do if we couldn't find a column? + return false; } + const operationsForNewField = + operationFieldSupportMatrix.operationByField[droppedItem.field.name]; + + const layerId = props.layerId; + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; + + // We need to check if dragging in a new field, was just a field change on the same + // index pattern and on the same operations (therefore checking if the new field supports + // our previous operation) + const hasFieldChanged = + selectedColumn && + hasField(selectedColumn) && + selectedColumn.sourceField !== droppedItem.field.name && + operationsForNewField && + operationsForNewField.includes(selectedColumn.operationType); + + // If only the field has changed use the onFieldChange method on the operation to get the + // new column, otherwise use the regular buildColumn to get a new column. + const newColumn = hasFieldChanged + ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) + : buildColumn({ + op: operationsForNewField ? operationsForNewField[0] : undefined, + columns: props.state.layers[props.layerId].columns, + indexPattern: currentIndexPattern, + layerId, + suggestedPriority: props.suggestedPriority, + field: droppedItem.field, + previousColumn: selectedColumn, + }); + + trackUiEvent('drop_onto_dimension'); + const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); + trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); + + props.setState( + changeColumn({ + state: props.state, + layerId, + columnId: props.columnId, + newColumn, + // If the field has changed, the onFieldChange method needs to take care of everything including moving + // over params. If we create a new column above we want changeColumn to move over params. + keepParams: !hasFieldChanged, + }) + ); + + return true; +} + +export const IndexPatternDimensionTriggerComponent = function IndexPatternDimensionTrigger( + props: IndexPatternDimensionTriggerProps +) { + const layerId = props.layerId; + + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + + const { columnId, uniqueLabel } = props; + if (!selectedColumn) { + return null; + } + return ( + { + props.togglePopover(); + }} + data-test-subj="lns-dimensionTrigger" + aria-label={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + title={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + > + {uniqueLabel} + + ); +}; + +export const IndexPatternDimensionEditorComponent = function IndexPatternDimensionPanel( + props: IndexPatternDimensionEditorProps +) { + const layerId = props.layerId; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + return ( - - { - if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { - // TODO: What do we do if we couldn't find a column? - return; - } - - const operationsForNewField = - operationFieldSupportMatrix.operationByField[droppedItem.field.name]; - - // We need to check if dragging in a new field, was just a field change on the same - // index pattern and on the same operations (therefore checking if the new field supports - // our previous operation) - const hasFieldChanged = - selectedColumn && - hasField(selectedColumn) && - selectedColumn.sourceField !== droppedItem.field.name && - operationsForNewField && - operationsForNewField.includes(selectedColumn.operationType); - - // If only the field has changed use the onFieldChange method on the operation to get the - // new column, otherwise use the regular buildColumn to get a new column. - const newColumn = hasFieldChanged - ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) - : buildColumn({ - op: operationsForNewField ? operationsForNewField[0] : undefined, - columns: props.state.layers[props.layerId].columns, - indexPattern: currentIndexPattern, - layerId, - suggestedPriority: props.suggestedPriority, - field: droppedItem.field, - previousColumn: selectedColumn, - }); - - trackUiEvent('drop_onto_dimension'); - const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); - trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); - - props.setState( - changeColumn({ - state: props.state, - layerId, - columnId: props.columnId, - newColumn, - // If the field has changed, the onFieldChange method needs to take care of everything including moving - // over params. If we create a new column above we want changeColumn to move over params. - keepParams: !hasFieldChanged, - }) - ); - }} - > - - {selectedColumn && ( - { - trackUiEvent('indexpattern_dimension_removed'); - props.setState( - deleteColumn({ - state: props.state, - layerId, - columnId: props.columnId, - }) - ); - if (props.onRemove) { - props.onRemove(props.columnId); - } - }} - /> - )} - - + ); }; -export const IndexPatternDimensionPanel = memo(IndexPatternDimensionPanelComponent); +export const IndexPatternDimensionTrigger = memo(IndexPatternDimensionTriggerComponent); +export const IndexPatternDimensionEditor = memo(IndexPatternDimensionEditorComponent); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index 056a8d177dfe8..e26c338b6e240 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -7,22 +7,18 @@ import _ from 'lodash'; import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiPopover, EuiFlexItem, EuiFlexGroup, EuiSideNav, EuiCallOut, EuiFormRow, EuiFieldText, - EuiLink, - EuiButtonEmpty, EuiSpacer, } from '@elastic/eui'; import classNames from 'classnames'; import { IndexPatternColumn, OperationType } from '../indexpattern'; -import { IndexPatternDimensionPanelProps, OperationFieldSupportMatrix } from './dimension_panel'; +import { IndexPatternDimensionEditorProps, OperationFieldSupportMatrix } from './dimension_panel'; import { operationDefinitionMap, getOperationDisplay, @@ -39,7 +35,7 @@ import { FormatSelector } from './format_selector'; const operationPanels = getOperationDisplay(); -export interface PopoverEditorProps extends IndexPatternDimensionPanelProps { +export interface PopoverEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: IndexPatternColumn; operationFieldSupportMatrix: OperationFieldSupportMatrix; currentIndexPattern: IndexPattern; @@ -67,11 +63,9 @@ export function PopoverEditor(props: PopoverEditorProps) { setState, layerId, currentIndexPattern, - uniqueLabel, hideGrouping, } = props; const { operationByField, fieldByOperation } = operationFieldSupportMatrix; - const [isPopoverOpen, setPopoverOpen] = useState(false); const [ incompatibleSelectedOperationType, setInvalidOperationType, @@ -115,14 +109,14 @@ export function PopoverEditor(props: PopoverEditorProps) { items: getOperationTypes().map(({ operationType, compatibleWithCurrentField }) => ({ name: operationPanels[operationType].displayName, id: operationType as string, - className: classNames('lnsPopoverEditor__operation', { - 'lnsPopoverEditor__operation--selected': Boolean( + className: classNames('lnsIndexPatternDimensionEditor__operation', { + 'lnsIndexPatternDimensionEditor__operation--selected': Boolean( incompatibleSelectedOperationType === operationType || (!incompatibleSelectedOperationType && selectedColumn && selectedColumn.operationType === operationType) ), - 'lnsPopoverEditor__operation--incompatible': !compatibleWithCurrentField, + 'lnsIndexPatternDimensionEditor__operation--incompatible': !compatibleWithCurrentField, }), 'data-test-subj': `lns-indexPatternDimension${ compatibleWithCurrentField ? '' : 'Incompatible' @@ -188,246 +182,193 @@ export function PopoverEditor(props: PopoverEditorProps) { } return ( - { - setPopoverOpen(!isPopoverOpen); +
+ + + { + setState( + deleteColumn({ + state, + layerId, + columnId, + }) + ); }} - data-test-subj="indexPattern-configure-dimension" - aria-label={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - title={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - > - {uniqueLabel} - - ) : ( - <> - setPopoverOpen(!isPopoverOpen)} - size="xs" - > - - - - ) - } - isOpen={isPopoverOpen} - closePopover={() => { - setPopoverOpen(false); - setInvalidOperationType(null); - }} - anchorPosition="leftUp" - withTitle - panelPaddingSize="s" - > - {isPopoverOpen && ( - - - { - setState( - deleteColumn({ - state, - layerId, - columnId, - }) - ); - }} - onChoose={choice => { - let column: IndexPatternColumn; - if ( - !incompatibleSelectedOperationType && - selectedColumn && - 'field' in choice && - choice.operationType === selectedColumn.operationType - ) { - // If we just changed the field are not in an error state and the operation didn't change, - // we use the operations onFieldChange method to calculate the new column. - column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); - } else { - // Otherwise we'll use the buildColumn method to calculate a new column - const compatibleOperations = - ('field' in choice && - operationFieldSupportMatrix.operationByField[choice.field]) || - []; - let operation; - if (compatibleOperations.length > 0) { - operation = - incompatibleSelectedOperationType && - compatibleOperations.includes(incompatibleSelectedOperationType) - ? incompatibleSelectedOperationType - : compatibleOperations[0]; - } else if ('field' in choice) { - operation = choice.operationType; - } - column = buildColumn({ - columns: props.state.layers[props.layerId].columns, - field: fieldMap[choice.field], - indexPattern: currentIndexPattern, - layerId: props.layerId, - suggestedPriority: props.suggestedPriority, - op: operation as OperationType, - previousColumn: selectedColumn, - }); + onChoose={choice => { + let column: IndexPatternColumn; + if ( + !incompatibleSelectedOperationType && + selectedColumn && + 'field' in choice && + choice.operationType === selectedColumn.operationType + ) { + // If we just changed the field are not in an error state and the operation didn't change, + // we use the operations onFieldChange method to calculate the new column. + column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); + } else { + // Otherwise we'll use the buildColumn method to calculate a new column + const compatibleOperations = + ('field' in choice && + operationFieldSupportMatrix.operationByField[choice.field]) || + []; + let operation; + if (compatibleOperations.length > 0) { + operation = + incompatibleSelectedOperationType && + compatibleOperations.includes(incompatibleSelectedOperationType) + ? incompatibleSelectedOperationType + : compatibleOperations[0]; + } else if ('field' in choice) { + operation = choice.operationType; } + column = buildColumn({ + columns: props.state.layers[props.layerId].columns, + field: fieldMap[choice.field], + indexPattern: currentIndexPattern, + layerId: props.layerId, + suggestedPriority: props.suggestedPriority, + op: operation as OperationType, + previousColumn: selectedColumn, + }); + } - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: column, - keepParams: false, - }) - ); - setInvalidOperationType(null); - }} - /> - - - - - - - - {incompatibleSelectedOperationType && selectedColumn && ( - - )} - {incompatibleSelectedOperationType && !selectedColumn && ( - - )} - {!incompatibleSelectedOperationType && ParamEditor && ( - <> - - - - )} - {!incompatibleSelectedOperationType && selectedColumn && ( - - { - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: { - ...selectedColumn, - label: e.target.value, - }, - }) - ); - }} - /> - - )} - - {!hideGrouping && ( - { - setState({ - ...state, - layers: { - ...state.layers, - [props.layerId]: { - ...state.layers[props.layerId], - columnOrder, - }, - }, - }); - }} + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: column, + keepParams: false, + }) + ); + setInvalidOperationType(null); + }} + /> + + + + + + + + {incompatibleSelectedOperationType && selectedColumn && ( + + )} + {incompatibleSelectedOperationType && !selectedColumn && ( + + )} + {!incompatibleSelectedOperationType && ParamEditor && ( + <> + - )} - - {selectedColumn && selectedColumn.dataType === 'number' ? ( - { + + + )} + {!incompatibleSelectedOperationType && selectedColumn && ( + + { setState( - updateColumnParam({ + changeColumn({ state, layerId, - currentColumn: selectedColumn, - paramName: 'format', - value: newFormat, + columnId, + newColumn: { + ...selectedColumn, + label: e.target.value, + }, }) ); }} /> - ) : null} - - - - - )} - + + )} + + {!hideGrouping && ( + { + setState({ + ...state, + layers: { + ...state.layers, + [props.layerId]: { + ...state.layers[props.layerId], + columnOrder, + }, + }, + }); + }} + /> + )} + + {selectedColumn && selectedColumn.dataType === 'number' ? ( + { + setState( + updateColumnParam({ + state, + layerId, + currentColumn: selectedColumn, + paramName: 'format', + value: newFormat, + }) + ); + }} + /> + ) : null} + + + + +
); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 25121eec30f2a..76e59a170a9e9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -408,7 +408,6 @@ describe('IndexPattern Data Source', () => { const initialState = stateFromPersistedState(persistedState); publicAPI = indexPatternDatasource.getPublicAPI({ state: initialState, - setState: () => {}, layerId: 'first', dateRange: { fromDate: 'now-30d', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 00f52d6a1747f..9c2a9c9bf4a09 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -12,7 +12,8 @@ import { CoreStart } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { - DatasourceDimensionPanelProps, + DatasourceDimensionEditorProps, + DatasourceDimensionTriggerProps, DatasourceDataPanelProps, Operation, DatasourceLayerPanelProps, @@ -20,7 +21,12 @@ import { } from '../types'; import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader'; import { toExpression } from './to_expression'; -import { IndexPatternDimensionPanel } from './dimension_panel'; +import { + IndexPatternDimensionTrigger, + IndexPatternDimensionEditor, + canHandleDrop, + onDrop, +} from './dimension_panel'; import { IndexPatternDataPanel } from './datapanel'; import { getDatasourceSuggestionsForField, @@ -38,6 +44,7 @@ import { } from './types'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Plugin as DataPlugin } from '../../../../../../src/plugins/data/public'; +import { deleteColumn } from './state_helpers'; import { Datasource, StateSetter } from '..'; export { OperationType, IndexPatternColumn } from './operations'; @@ -80,6 +87,9 @@ export function uniqueLabels(layers: Record) { }; Object.values(layers).forEach(layer => { + if (!layer.columns) { + return; + } Object.entries(layer.columns).forEach(([columnId, column]) => { columnLabelMap[columnId] = makeUnique(column.label); }); @@ -156,6 +166,14 @@ export function getIndexPatternDatasource({ return Object.keys(state.layers); }, + removeColumn({ prevState, layerId, columnId }) { + return deleteColumn({ + state: prevState, + layerId, + columnId, + }); + }, + toExpression, getMetaData(state: IndexPatternPrivateState) { @@ -198,15 +216,97 @@ export function getIndexPatternDatasource({ ); }, - getPublicAPI({ - state, - setState, - layerId, - dateRange, - }: PublicAPIProps) { + renderDimensionTrigger: ( + domElement: Element, + props: DatasourceDimensionTriggerProps + ) => { + const columnLabelMap = uniqueLabels(props.state.layers); + + render( + + + + + , + domElement + ); + }, + + renderDimensionEditor: ( + domElement: Element, + props: DatasourceDimensionEditorProps + ) => { + const columnLabelMap = uniqueLabels(props.state.layers); + + render( + + + + + , + domElement + ); + }, + + renderLayerPanel: ( + domElement: Element, + props: DatasourceLayerPanelProps + ) => { + render( + { + changeLayerIndexPattern({ + savedObjectsClient, + indexPatternId, + setState: props.setState, + state: props.state, + layerId: props.layerId, + onError: onIndexPatternLoadError, + replaceIfPossible: true, + }); + }} + {...props} + />, + domElement + ); + }, + + canHandleDrop, + onDrop, + + getPublicAPI({ state, layerId }: PublicAPIProps) { const columnLabelMap = uniqueLabels(state.layers); return { + datasourceId: 'indexpattern', + getTableSpec: () => { return state.layers[layerId].columnOrder.map(colId => ({ columnId: colId })); }, @@ -218,58 +318,6 @@ export function getIndexPatternDatasource({ } return null; }, - renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => { - render( - - - - - , - domElement - ); - }, - - renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => { - render( - { - changeLayerIndexPattern({ - savedObjectsClient, - indexPatternId, - setState, - state, - layerId: props.layerId, - onError: onIndexPatternLoadError, - replaceIfPossible: true, - }); - }} - {...props} - />, - domElement - ); - }, }; }, getDatasourceSuggestionsForField(state, draggedField) { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index af7afb9cf9342..219a6d935e436 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -178,6 +178,7 @@ describe('Layer Data Panel', () => { defaultProps = { layerId: 'first', state: initialState, + setState: jest.fn(), onChangeIndexPattern: jest.fn(async () => {}), }; }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index ae346ecc72cbc..eea00d52a77f9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -11,7 +11,8 @@ import { DatasourceLayerPanelProps } from '../types'; import { IndexPatternPrivateState } from './types'; import { ChangeIndexPattern } from './change_indexpattern'; -export interface IndexPatternLayerPanelProps extends DatasourceLayerPanelProps { +export interface IndexPatternLayerPanelProps + extends DatasourceLayerPanelProps { state: IndexPatternPrivateState; onChangeIndexPattern: (newId: string) => void; } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx deleted file mode 100644 index eac35f82a50fa..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { ReactWrapper } from 'enzyme'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; -import { MetricConfigPanel } from './metric_config_panel'; -import { DatasourceDimensionPanelProps, Operation, DatasourcePublicAPI } from '../types'; -import { State } from './types'; -import { NativeRendererProps } from '../native_renderer'; -import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; - -describe('MetricConfigPanel', () => { - const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; - - function mockDatasource(): DatasourcePublicAPI { - return createMockDatasource().publicAPIMock; - } - - function testState(): State { - return { - accessor: 'foo', - layerId: 'bar', - }; - } - - function testSubj(component: ReactWrapper, subj: string) { - return component - .find(`[data-test-subj="${subj}"]`) - .first() - .props(); - } - - test('the value dimension panel only accepts singular numeric operations', () => { - const state = testState(); - const component = mount( - - ); - - const panel = testSubj(component, 'lns_metric_valueDimensionPanel'); - const nativeProps = (panel as NativeRendererProps).nativeProps; - const { columnId, filterOperations } = nativeProps; - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const ops: Operation[] = [ - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(columnId).toEqual('shazm'); - expect(ops.filter(filterOperations)).toEqual([{ ...exampleOperation, dataType: 'number' }]); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx deleted file mode 100644 index 16e24f247fb68..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFormRow } from '@elastic/eui'; -import { State } from './types'; -import { VisualizationLayerConfigProps, OperationMetadata } from '../types'; -import { NativeRenderer } from '../native_renderer'; - -const isMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; - -export function MetricConfigPanel(props: VisualizationLayerConfigProps) { - const { state, frame, layerId } = props; - const datasource = frame.datasourceLayers[layerId]; - - return ( - - - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx index 66ed963002f59..4d979a766cd2b 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx @@ -91,6 +91,11 @@ export function MetricChart({ const { title, accessor, mode } = args; let value = '-'; const firstTable = Object.values(data.tables)[0]; + if (!accessor) { + return ( + + ); + } if (firstTable) { const column = firstTable.columns[0]; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts index 88964b95c2ac7..276f24433c670 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -24,8 +24,8 @@ function mockFrame(): FramePublicAPI { ...createMockFramePublicAPI(), addNewLayer: () => 'l42', datasourceLayers: { - l1: createMockDatasource().publicAPIMock, - l42: createMockDatasource().publicAPIMock, + l1: createMockDatasource('l1').publicAPIMock, + l42: createMockDatasource('l42').publicAPIMock, }, }; } @@ -36,10 +36,10 @@ describe('metric_visualization', () => { (generateId as jest.Mock).mockReturnValueOnce('test-id1'); const initialState = metricVisualization.initialize(mockFrame()); - expect(initialState.accessor).toBeDefined(); + expect(initialState.accessor).not.toBeDefined(); expect(initialState).toMatchInlineSnapshot(` Object { - "accessor": "test-id1", + "accessor": undefined, "layerId": "l42", } `); @@ -60,7 +60,7 @@ describe('metric_visualization', () => { it('returns a clean layer', () => { (generateId as jest.Mock).mockReturnValueOnce('test-id1'); expect(metricVisualization.clearLayer(exampleState(), 'l1')).toEqual({ - accessor: 'test-id1', + accessor: undefined, layerId: 'l1', }); }); @@ -72,10 +72,47 @@ describe('metric_visualization', () => { }); }); + describe('#setDimension', () => { + it('sets the accessor', () => { + expect( + metricVisualization.setDimension({ + prevState: { + accessor: undefined, + layerId: 'l1', + }, + layerId: 'l1', + groupId: '', + columnId: 'newDimension', + }) + ).toEqual({ + accessor: 'newDimension', + layerId: 'l1', + }); + }); + }); + + describe('#removeDimension', () => { + it('removes the accessor', () => { + expect( + metricVisualization.removeDimension({ + prevState: { + accessor: 'a', + layerId: 'l1', + }, + layerId: 'l1', + columnId: 'a', + }) + ).toEqual({ + accessor: undefined, + layerId: 'l1', + }); + }); + }); + describe('#toExpression', () => { it('should map to a valid AST', () => { const datasource: DatasourcePublicAPI = { - ...createMockDatasource().publicAPIMock, + ...createMockDatasource('l1').publicAPIMock, getOperationForColumnId(_: string) { return { id: 'a', diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx index 6714c05787837..44256df5aed6d 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -4,23 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { render } from 'react-dom'; -import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Ast } from '@kbn/interpreter/target/common'; import { getSuggestions } from './metric_suggestions'; -import { MetricConfigPanel } from './metric_config_panel'; -import { Visualization, FramePublicAPI } from '../types'; +import { Visualization, FramePublicAPI, OperationMetadata } from '../types'; import { State, PersistableState } from './types'; -import { generateId } from '../id_generator'; import chartMetricSVG from '../assets/chart_metric.svg'; const toExpression = ( state: State, frame: FramePublicAPI, mode: 'reduced' | 'full' = 'full' -): Ast => { +): Ast | null => { + if (!state.accessor) { + return null; + } + const [datasource] = Object.values(frame.datasourceLayers); const operation = datasource && datasource.getOperationForColumnId(state.accessor); @@ -57,7 +56,7 @@ export const metricVisualization: Visualization = { clearLayer(state) { return { ...state, - accessor: generateId(), + accessor: undefined, }; }, @@ -80,22 +79,37 @@ export const metricVisualization: Visualization = { return ( state || { layerId: frame.addNewLayer(), - accessor: generateId(), + accessor: undefined, } ); }, getPersistableState: state => state, - renderLayerConfigPanel: (domElement, props) => - render( - - - , - domElement - ), + getConfiguration(props) { + return { + groups: [ + { + groupId: 'metric', + groupLabel: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric' }), + layerId: props.state.layerId, + accessors: props.state.accessor ? [props.state.accessor] : [], + supportsMoreColumns: false, + filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number', + }, + ], + }; + }, toExpression, toPreviewExpression: (state: State, frame: FramePublicAPI) => toExpression(state, frame, 'reduced'), + + setDimension({ prevState, columnId }) { + return { ...prevState, accessor: columnId }; + }, + + removeDimension({ prevState }) { + return { ...prevState, accessor: undefined }; + }, }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts index 6348d80b15e2f..53fc103934255 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts @@ -6,7 +6,7 @@ export interface State { layerId: string; - accessor: string; + accessor?: string; } export interface MetricConfig extends State { diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx deleted file mode 100644 index 38f48c9cdaf72..0000000000000 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { createMockDatasource } from '../editor_frame_service/mocks'; -import { MultiColumnEditor } from './multi_column_editor'; -import { mount } from 'enzyme'; - -jest.useFakeTimers(); - -describe('MultiColumnEditor', () => { - it('should add a trailing accessor if the accessor list is empty', () => { - const onAdd = jest.fn(); - mount( - true} - layerId="foo" - onAdd={onAdd} - onRemove={jest.fn()} - testSubj="bar" - /> - ); - - expect(onAdd).toHaveBeenCalledTimes(0); - - jest.runAllTimers(); - - expect(onAdd).toHaveBeenCalledTimes(1); - }); - - it('should add a trailing accessor if the last accessor is configured', () => { - const onAdd = jest.fn(); - mount( - true} - layerId="foo" - onAdd={onAdd} - onRemove={jest.fn()} - testSubj="bar" - /> - ); - - expect(onAdd).toHaveBeenCalledTimes(0); - - jest.runAllTimers(); - - expect(onAdd).toHaveBeenCalledTimes(1); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx deleted file mode 100644 index 422f1dcf60f3c..0000000000000 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect } from 'react'; -import { NativeRenderer } from '../native_renderer'; -import { DatasourcePublicAPI, OperationMetadata } from '../types'; -import { DragContextState } from '../drag_drop'; - -interface Props { - accessors: string[]; - datasource: DatasourcePublicAPI; - dragDropContext: DragContextState; - onRemove: (accessor: string) => void; - onAdd: () => void; - filterOperations: (op: OperationMetadata) => boolean; - suggestedPriority?: 0 | 1 | 2 | undefined; - testSubj: string; - layerId: string; -} - -export function MultiColumnEditor({ - accessors, - datasource, - dragDropContext, - onRemove, - onAdd, - filterOperations, - suggestedPriority, - testSubj, - layerId, -}: Props) { - const lastOperation = datasource.getOperationForColumnId(accessors[accessors.length - 1]); - - useEffect(() => { - if (accessors.length === 0 || lastOperation !== null) { - setTimeout(onAdd); - } - }, [lastOperation]); - - return ( - <> - {accessors.map(accessor => ( -
- -
- ))} - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index cc029fee49d1d..c74653c70703c 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -114,7 +114,7 @@ export class LensPlugin { const savedObjectsClient = coreStart.savedObjects.client; addHelpMenuToAppChrome(coreStart.chrome); - const instance = await this.createEditorFrame!({}); + const instance = await this.createEditorFrame!(); setReportManager( new LensReportManager({ diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index b7983eeb8dbb8..c897979b06cfb 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -13,14 +13,10 @@ import { Document } from './persistence'; import { DateRange } from '../../../../plugins/lens/common'; import { Query, Filter, SavedQuery } from '../../../../../src/plugins/data/public'; -// eslint-disable-next-line -export interface EditorFrameOptions {} - export type ErrorCallback = (e: { message: string }) => void; export interface PublicAPIProps { state: T; - setState: StateSetter; layerId: string; dateRange: DateRange; } @@ -34,6 +30,7 @@ export interface EditorFrameProps { savedQuery?: SavedQuery; // Frame loader (app or embeddable) is expected to call this when it loads and updates + // This should be replaced with a top-down state onChange: (newState: { filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; doc: Document; @@ -53,7 +50,7 @@ export interface EditorFrameSetup { } export interface EditorFrameStart { - createInstance: (options: EditorFrameOptions) => Promise; + createInstance: () => Promise; } // Hints the default nesting to the data source. 0 is the highest priority @@ -138,8 +135,14 @@ export interface Datasource { removeLayer: (state: T, layerId: string) => T; clearLayer: (state: T, layerId: string) => T; getLayers: (state: T) => string[]; + removeColumn: (props: { prevState: T; layerId: string; columnId: string }) => T; renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; + renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; + renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; + renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; + canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; + onDrop: (props: DatasourceDimensionDropHandlerProps) => boolean; toExpression: (state: T, layerId: string) => Ast | string | null; @@ -155,22 +158,11 @@ export interface Datasource { * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource */ export interface DatasourcePublicAPI { - getTableSpec: () => TableSpec; + datasourceId: string; + getTableSpec: () => Array<{ columnId: string }>; getOperationForColumnId: (columnId: string) => Operation | null; - - // Render can be called many times - renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => void; - renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; } -export interface TableSpecColumn { - // Column IDs are the keys for internal state in data sources and visualizations - columnId: string; -} - -// TableSpec is managed by visualizations -export type TableSpec = TableSpecColumn[]; - export interface DatasourceDataPanelProps { state: T; dragDropContext: DragContextState; @@ -181,31 +173,61 @@ export interface DatasourceDataPanelProps { filters: Filter[]; } -// The only way a visualization has to restrict the query building -export interface DatasourceDimensionPanelProps { - layerId: string; - columnId: string; - - dragDropContext: DragContextState; - - // Visualizations can restrict operations based on their own rules +interface SharedDimensionProps { + /** Visualizations can restrict operations based on their own rules. + * For example, limiting to only bucketed or only numeric operations. + */ filterOperations: (operation: OperationMetadata) => boolean; - // Visualizations can hint at the role this dimension would play, which - // affects the default ordering of the query + /** Visualizations can hint at the role this dimension would play, which + * affects the default ordering of the query + */ suggestedPriority?: DimensionPriority; - onRemove?: (accessor: string) => void; - // Some dimension editors will allow users to change the operation grouping - // from the panel, and this lets the visualization hint that it doesn't want - // users to have that level of control + /** Some dimension editors will allow users to change the operation grouping + * from the panel, and this lets the visualization hint that it doesn't want + * users to have that level of control + */ hideGrouping?: boolean; } -export interface DatasourceLayerPanelProps { +export type DatasourceDimensionProps = SharedDimensionProps & { layerId: string; + columnId: string; + onRemove?: (accessor: string) => void; + state: T; +}; + +// The only way a visualization has to restrict the query building +export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { + setState: StateSetter; + core: Pick; + dateRange: DateRange; +}; + +export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { + dragDropContext: DragContextState; + togglePopover: () => void; +}; + +export interface DatasourceLayerPanelProps { + layerId: string; + state: T; + setState: StateSetter; } +export type DatasourceDimensionDropProps = SharedDimensionProps & { + layerId: string; + columnId: string; + state: T; + setState: StateSetter; + dragDropContext: DragContextState; +}; + +export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { + droppedItem: unknown; +}; + export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip'; // An operation represents a column in a table, not any information @@ -239,12 +261,32 @@ export interface LensMultiTable { }; } -export interface VisualizationLayerConfigProps { +export interface VisualizationConfigProps { layerId: string; - dragDropContext: DragContextState; frame: FramePublicAPI; state: T; +} + +export type VisualizationLayerWidgetProps = VisualizationConfigProps & { setState: (newState: T) => void; +}; + +type VisualizationDimensionGroupConfig = SharedDimensionProps & { + groupLabel: string; + + /** ID is passed back to visualization. For example, `x` */ + groupId: string; + accessors: string[]; + supportsMoreColumns: boolean; + /** If required, a warning will appear if accessors are empty */ + required?: boolean; + dataTestSubj?: string; +}; + +interface VisualizationDimensionChangeProps { + layerId: string; + columnId: string; + prevState: T; } /** @@ -329,16 +371,18 @@ export interface Visualization { visualizationTypes: VisualizationType[]; getLayerIds: (state: T) => string[]; - clearLayer: (state: T, layerId: string) => T; - removeLayer?: (state: T, layerId: string) => T; - appendLayer?: (state: T, layerId: string) => T; + // Layer context menu is used by visualizations for styling the entire layer + // For example, the XY visualization uses this to have multiple chart types getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; + renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; - renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerConfigProps) => void; + getConfiguration: ( + props: VisualizationConfigProps + ) => { groups: VisualizationDimensionGroupConfig[] }; getDescription: ( state: T @@ -354,7 +398,13 @@ export interface Visualization { getPersistableState: (state: T) => P; - renderLayerConfigPanel: (domElement: Element, props: VisualizationLayerConfigProps) => void; + // Actions triggered by the frame which tell the datasource that a dimension is being changed + setDimension: ( + props: VisualizationDimensionChangeProps & { + groupId: string; + } + ) => T; + removeDimension: (props: VisualizationDimensionChangeProps) => T; toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap similarity index 96% rename from x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap rename to x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 76af8328673ad..6b68679bfd4ec 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`xy_visualization #toExpression should map to a valid AST 1`] = ` +exports[`#toExpression should map to a valid AST 1`] = ` Object { "chain": Array [ Object { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts new file mode 100644 index 0000000000000..6bc379ea33bca --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Ast } from '@kbn/interpreter/target/common'; +import { Position } from '@elastic/charts'; +import { xyVisualization } from './xy_visualization'; +import { Operation } from '../types'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; + +describe('#toExpression', () => { + let mockDatasource: ReturnType; + let frame: ReturnType; + + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDatasource = createMockDatasource('testDatasource'); + + mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ + { columnId: 'd' }, + { columnId: 'a' }, + { columnId: 'b' }, + { columnId: 'c' }, + ]); + + mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { + return { label: `col_${col}`, dataType: 'number' } as Operation; + }); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + + it('should map to a valid AST', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], + }, + frame + ) + ).toMatchSnapshot(); + }); + + it('should not generate an expression when missing x', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: undefined, + xAccessor: undefined, + accessors: ['a'], + }, + ], + }, + frame + ) + ).toBeNull(); + }); + + it('should not generate an expression when missing y', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: undefined, + xAccessor: 'a', + accessors: [], + }, + ], + }, + frame + ) + ).toBeNull(); + }); + + it('should default to labeling all columns with their column label', () => { + const expression = xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], + }, + frame + )! as Ast; + + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); + expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']); + expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']); + expect( + (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel + ).toEqual([ + JSON.stringify({ + b: 'col_b', + c: 'col_c', + d: 'col_d', + }), + ]); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts index f0e932d14f281..9b068b0ca5ef0 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts @@ -9,6 +9,10 @@ import { ScaleType } from '@elastic/charts'; import { State, LayerConfig } from './types'; import { FramePublicAPI, OperationMetadata } from '../types'; +interface ValidLayer extends LayerConfig { + xAccessor: NonNullable; +} + function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { const defaults = { xTitle: 'x', @@ -22,8 +26,8 @@ function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { if (!datasource) { return defaults; } - const x = datasource.getOperationForColumnId(layer.xAccessor); - const y = datasource.getOperationForColumnId(layer.accessors[0]); + const x = layer.xAccessor ? datasource.getOperationForColumnId(layer.xAccessor) : null; + const y = layer.accessors[0] ? datasource.getOperationForColumnId(layer.accessors[0]) : null; return { xTitle: x ? x.label : defaults.xTitle, @@ -36,26 +40,6 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => return null; } - const stateWithValidAccessors = { - ...state, - layers: state.layers.map(layer => { - const datasource = frame.datasourceLayers[layer.layerId]; - - const newLayer = { ...layer }; - - if (!datasource.getOperationForColumnId(layer.splitAccessor)) { - delete newLayer.splitAccessor; - } - - return { - ...newLayer, - accessors: layer.accessors.filter(accessor => - Boolean(datasource.getOperationForColumnId(accessor)) - ), - }; - }), - }; - const metadata: Record> = {}; state.layers.forEach(layer => { metadata[layer.layerId] = {}; @@ -68,12 +52,7 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => }); }); - return buildExpression( - stateWithValidAccessors, - metadata, - frame, - xyTitles(state.layers[0], frame) - ); + return buildExpression(state, metadata, frame, xyTitles(state.layers[0], frame)); }; export function toPreviewExpression(state: State, frame: FramePublicAPI) { @@ -122,82 +101,94 @@ export const buildExpression = ( metadata: Record>, frame?: FramePublicAPI, { xTitle, yTitle }: { xTitle: string; yTitle: string } = { xTitle: '', yTitle: '' } -): Ast => ({ - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_chart', - arguments: { - xTitle: [xTitle], - yTitle: [yTitle], - legend: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_legendConfig', - arguments: { - isVisible: [state.legend.isVisible], - position: [state.legend.position], +): Ast | null => { + const validLayers = state.layers.filter((layer): layer is ValidLayer => + Boolean(layer.xAccessor && layer.accessors.length) + ); + if (!validLayers.length) { + return null; + } + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_chart', + arguments: { + xTitle: [xTitle], + yTitle: [yTitle], + legend: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_legendConfig', + arguments: { + isVisible: [state.legend.isVisible], + position: [state.legend.position], + }, }, - }, - ], - }, - ], - layers: state.layers.map(layer => { - const columnToLabel: Record = {}; - - if (frame) { - const datasource = frame.datasourceLayers[layer.layerId]; - layer.accessors.concat([layer.splitAccessor]).forEach(accessor => { - const operation = datasource.getOperationForColumnId(accessor); - if (operation && operation.label) { - columnToLabel[accessor] = operation.label; - } - }); - } - - const xAxisOperation = - frame && frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); - - const isHistogramDimension = Boolean( - xAxisOperation && - xAxisOperation.isBucketed && - xAxisOperation.scale && - xAxisOperation.scale !== 'ordinal' - ); - - return { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_layer', - arguments: { - layerId: [layer.layerId], - - hide: [Boolean(layer.hide)], - - xAccessor: [layer.xAccessor], - yScaleType: [ - getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), - ], - xScaleType: [ - getScaleType(metadata[layer.layerId][layer.xAccessor], ScaleType.Linear), - ], - isHistogram: [isHistogramDimension], - splitAccessor: [layer.splitAccessor], - seriesType: [layer.seriesType], - accessors: layer.accessors, - columnToLabel: [JSON.stringify(columnToLabel)], + ], + }, + ], + layers: validLayers.map(layer => { + const columnToLabel: Record = {}; + + if (frame) { + const datasource = frame.datasourceLayers[layer.layerId]; + layer.accessors + .concat(layer.splitAccessor ? [layer.splitAccessor] : []) + .forEach(accessor => { + const operation = datasource.getOperationForColumnId(accessor); + if (operation && operation.label) { + columnToLabel[accessor] = operation.label; + } + }); + } + + const xAxisOperation = + frame && + frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); + + const isHistogramDimension = Boolean( + xAxisOperation && + xAxisOperation.isBucketed && + xAxisOperation.scale && + xAxisOperation.scale !== 'ordinal' + ); + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_layer', + arguments: { + layerId: [layer.layerId], + + hide: [Boolean(layer.hide)], + + xAccessor: [layer.xAccessor], + yScaleType: [ + getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), + ], + xScaleType: [ + getScaleType(metadata[layer.layerId][layer.xAccessor], ScaleType.Linear), + ], + isHistogram: [isHistogramDimension], + splitAccessor: layer.splitAccessor ? [layer.splitAccessor] : [], + seriesType: [layer.seriesType], + accessors: layer.accessors, + columnToLabel: [JSON.stringify(columnToLabel)], + }, }, - }, - ], - }; - }), + ], + }; + }), + }, }, - }, - ], -}); + ], + }; +}; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts index b49e6fa6b4b6f..f7b4afc76ec4b 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts @@ -191,10 +191,10 @@ export type SeriesType = export interface LayerConfig { hide?: boolean; layerId: string; - xAccessor: string; + xAccessor?: string; accessors: string[]; seriesType: SeriesType; - splitAccessor: string; + splitAccessor?: string; } export type LayerArgs = LayerConfig & { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 301c4a58a0ffd..7544ed0f87b7d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -5,22 +5,15 @@ */ import React from 'react'; -import { ReactWrapper } from 'enzyme'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps } from '@elastic/eui'; -import { XYConfigPanel, LayerContextMenu } from './xy_config_panel'; -import { DatasourceDimensionPanelProps, Operation, FramePublicAPI } from '../types'; +import { LayerContextMenu } from './xy_config_panel'; +import { FramePublicAPI } from '../types'; import { State } from './types'; import { Position } from '@elastic/charts'; -import { NativeRendererProps } from '../native_renderer'; -import { generateId } from '../id_generator'; import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; -jest.mock('../id_generator'); - -describe('XYConfigPanel', () => { - const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; - +describe('LayerContextMenu', () => { let frame: FramePublicAPI; function testState(): State { @@ -39,17 +32,10 @@ describe('XYConfigPanel', () => { }; } - function testSubj(component: ReactWrapper, subj: string) { - return component - .find(`[data-test-subj="${subj}"]`) - .first() - .props(); - } - beforeEach(() => { frame = createMockFramePublicAPI(); frame.datasourceLayers = { - first: createMockDatasource().publicAPIMock, + first: createMockDatasource('test').publicAPIMock, }; }); @@ -64,7 +50,6 @@ describe('XYConfigPanel', () => { const component = mount( { const component = mount( { expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); }); }); - - test('the x dimension panel accepts only bucketed operations', () => { - // TODO: this should eventually also accept raw operation - const state = testState(); - const component = mount( - - ); - - const panel = testSubj(component, 'lnsXY_xDimensionPanel'); - const nativeProps = (panel as NativeRendererProps).nativeProps; - const { columnId, filterOperations } = nativeProps; - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const bucketedOps: Operation[] = [ - { ...exampleOperation, isBucketed: true, dataType: 'number' }, - { ...exampleOperation, isBucketed: true, dataType: 'string' }, - { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, - { ...exampleOperation, isBucketed: true, dataType: 'date' }, - ]; - const ops: Operation[] = [ - ...bucketedOps, - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(columnId).toEqual('shazm'); - expect(ops.filter(filterOperations)).toEqual(bucketedOps); - }); - - test('the y dimension panel accepts numeric operations', () => { - const state = testState(); - const component = mount( - - ); - - const filterOperations = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('filterOperations') as (op: Operation) => boolean; - - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const ops: Operation[] = [ - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(ops.filter(filterOperations).map(x => x.dataType)).toEqual(['number']); - }); - - test('allows removal of y dimensions', () => { - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - const onRemove = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('onRemove') as (accessor: string) => {}; - - onRemove('b'); - - expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0]).toMatchObject({ - layers: [ - { - ...state.layers[0], - accessors: ['a', 'c'], - }, - ], - }); - }); - - test('allows adding a y axis dimension', () => { - (generateId as jest.Mock).mockReturnValueOnce('zed'); - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - const onAdd = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('onAdd') as () => {}; - - onAdd(); - - expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0]).toMatchObject({ - layers: [ - { - ...state.layers[0], - accessors: ['a', 'b', 'c', 'zed'], - }, - ], - }); - }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx index dbcfa24395001..5e85680cc2b2c 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -9,16 +9,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; import { State, SeriesType, visualizationTypes } from './types'; -import { VisualizationLayerConfigProps, OperationMetadata } from '../types'; -import { NativeRenderer } from '../native_renderer'; -import { MultiColumnEditor } from '../multi_column_editor'; -import { generateId } from '../id_generator'; +import { VisualizationLayerWidgetProps } from '../types'; import { isHorizontalChart, isHorizontalSeries } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; -const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; -const isBucketed = (op: OperationMetadata) => op.isBucketed; - type UnwrapArray = T extends Array ? P : T; function updateLayer(state: State, layer: UnwrapArray, index: number): State { @@ -31,7 +25,7 @@ function updateLayer(state: State, layer: UnwrapArray, index: n }; } -export function LayerContextMenu(props: VisualizationLayerConfigProps) { +export function LayerContextMenu(props: VisualizationLayerWidgetProps) { const { state, layerId } = props; const horizontalOnly = isHorizontalChart(state.layers); const index = state.layers.findIndex(l => l.layerId === layerId); @@ -74,97 +68,3 @@ export function LayerContextMenu(props: VisualizationLayerConfigProps) { ); } - -export function XYConfigPanel(props: VisualizationLayerConfigProps) { - const { state, setState, frame, layerId } = props; - const index = props.state.layers.findIndex(l => l.layerId === layerId); - - if (index < 0) { - return null; - } - - const layer = props.state.layers[index]; - - return ( - <> - - - - - - setState( - updateLayer( - state, - { - ...layer, - accessors: [...layer.accessors, generateId()], - }, - index - ) - ) - } - onRemove={accessor => - setState( - updateLayer( - state, - { - ...layer, - accessors: layer.accessors.filter(col => col !== accessor), - }, - index - ) - ) - } - filterOperations={isNumericMetric} - data-test-subj="lensXY_yDimensionPanel" - testSubj="lensXY_yDimensionPanel" - layerId={layer.layerId} - /> - - - - - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx index 27fd6e7064042..15aaf289eebf9 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -238,6 +238,8 @@ export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYC index ) => { if ( + !xAccessor || + !accessors.length || !data.tables[layerId] || data.tables[layerId].rows.length === 0 || data.tables[layerId].rows.every(row => typeof row[xAccessor] === 'undefined') @@ -246,7 +248,7 @@ export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYC } const columnToLabelMap = columnToLabel ? JSON.parse(columnToLabel) : {}; - const splitAccessorLabel = columnToLabelMap[splitAccessor]; + const splitAccessorLabel = splitAccessor ? columnToLabelMap[splitAccessor] : ''; const yAccessors = accessors.map(accessor => columnToLabelMap[accessor] || accessor); const idForLegend = splitAccessorLabel || yAccessors; const sanitized = sanitizeRows({ diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 04ff720309d62..ddbd9d11b5fad 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -123,7 +123,7 @@ describe('xy_suggestions', () => { Array [ Object { "seriesType": "bar_stacked", - "splitAccessor": "aaa", + "splitAccessor": undefined, "x": "date", "y": Array [ "bytes", @@ -240,7 +240,6 @@ describe('xy_suggestions', () => { }); test('only makes a seriesType suggestion for unchanged table without split', () => { - (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', @@ -249,7 +248,7 @@ describe('xy_suggestions', () => { accessors: ['price'], layerId: 'first', seriesType: 'bar', - splitAccessor: 'dummyCol', + splitAccessor: undefined, xAccessor: 'date', }, ], @@ -472,17 +471,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "ddd", - "x": "quantity", - "y": Array [ - "price", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "quantity", + "y": Array [ + "price", + ], + }, + ] + `); }); test('handles ip', () => { @@ -509,17 +508,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "ddd", - "x": "myip", - "y": Array [ - "quantity", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "myip", + "y": Array [ + "quantity", + ], + }, + ] + `); }); test('handles unbucketed suggestions', () => { @@ -545,16 +544,16 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "eee", - "x": "mybool", - "y": Array [ - "num votes", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "mybool", + "y": Array [ + "num votes", + ], + }, + ] + `); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts index 33181b7f3a467..5e9311bb1e928 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -15,7 +15,6 @@ import { TableChangeType, } from '../types'; import { State, SeriesType, XYState } from './types'; -import { generateId } from '../id_generator'; import { getIconForSeries } from './state_helpers'; const columnSortOrder = { @@ -356,7 +355,7 @@ function buildSuggestion({ layerId, seriesType, xAccessor: xValue.columnId, - splitAccessor: splitBy ? splitBy.columnId : generateId(), + splitAccessor: splitBy?.columnId, accessors: yValues.map(col => col.columnId), }; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index a27a8e7754b86..beccf0dc46eb4 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -9,10 +9,6 @@ import { Position } from '@elastic/charts'; import { Operation } from '../types'; import { State, SeriesType } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; -import { generateId } from '../id_generator'; -import { Ast } from '@kbn/interpreter/target/common'; - -jest.mock('../id_generator'); function exampleState(): State { return { @@ -87,31 +83,22 @@ describe('xy_visualization', () => { describe('#initialize', () => { it('loads default state', () => { - (generateId as jest.Mock) - .mockReturnValueOnce('test-id1') - .mockReturnValueOnce('test-id2') - .mockReturnValue('test-id3'); const mockFrame = createMockFramePublicAPI(); const initialState = xyVisualization.initialize(mockFrame); expect(initialState.layers).toHaveLength(1); - expect(initialState.layers[0].xAccessor).toBeDefined(); - expect(initialState.layers[0].accessors[0]).toBeDefined(); - expect(initialState.layers[0].xAccessor).not.toEqual(initialState.layers[0].accessors[0]); + expect(initialState.layers[0].xAccessor).not.toBeDefined(); + expect(initialState.layers[0].accessors).toHaveLength(0); expect(initialState).toMatchInlineSnapshot(` Object { "layers": Array [ Object { - "accessors": Array [ - "test-id1", - ], + "accessors": Array [], "layerId": "", "position": "top", "seriesType": "bar_stacked", "showGridlines": false, - "splitAccessor": "test-id2", - "xAccessor": "test-id3", }, ], "legend": Object { @@ -167,14 +154,11 @@ describe('xy_visualization', () => { describe('#clearLayer', () => { it('clears the specified layer', () => { - (generateId as jest.Mock).mockReturnValue('test_empty_id'); const layer = xyVisualization.clearLayer(exampleState(), 'first').layers[0]; expect(layer).toMatchObject({ - accessors: ['test_empty_id'], + accessors: [], layerId: 'first', seriesType: 'bar', - splitAccessor: 'test_empty_id', - xAccessor: 'test_empty_id', }); }); }); @@ -185,13 +169,94 @@ describe('xy_visualization', () => { }); }); - describe('#toExpression', () => { + describe('#setDimension', () => { + it('sets the x axis', () => { + expect( + xyVisualization.setDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + }, + ], + }, + layerId: 'first', + groupId: 'x', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: 'newCol', + accessors: [], + }); + }); + + it('replaces the x axis', () => { + expect( + xyVisualization.setDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + layerId: 'first', + groupId: 'x', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: 'newCol', + accessors: [], + }); + }); + }); + + describe('#removeDimension', () => { + it('removes the x axis', () => { + expect( + xyVisualization.removeDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + layerId: 'first', + columnId: 'a', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + }); + }); + }); + + describe('#getConfiguration', () => { let mockDatasource: ReturnType; let frame: ReturnType; beforeEach(() => { frame = createMockFramePublicAPI(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('testDatasource'); mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ { columnId: 'd' }, @@ -200,36 +265,78 @@ describe('xy_visualization', () => { { columnId: 'c' }, ]); - mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { - return { label: `col_${col}`, dataType: 'number' } as Operation; - }); - frame.datasourceLayers = { first: mockDatasource.publicAPIMock, }; }); - it('should map to a valid AST', () => { - expect(xyVisualization.toExpression(exampleState(), frame)).toMatchSnapshot(); + it('should return options for 3 dimensions', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + expect(options).toHaveLength(3); + expect(options.map(o => o.groupId)).toEqual(['x', 'y', 'breakdown']); }); - it('should default to labeling all columns with their column label', () => { - const expression = xyVisualization.toExpression(exampleState(), frame)! as Ast; + it('should only accept bucketed operations for x', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + const filterOperations = options.find(o => o.groupId === 'x')!.filterOperations; - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); - expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']); - expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']); - expect( - (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel - ).toEqual([ - JSON.stringify({ - b: 'col_b', - c: 'col_c', - d: 'col_d', - }), - ]); + const exampleOperation: Operation = { + dataType: 'number', + isBucketed: false, + label: 'bar', + }; + const bucketedOps: Operation[] = [ + { ...exampleOperation, isBucketed: true, dataType: 'number' }, + { ...exampleOperation, isBucketed: true, dataType: 'string' }, + { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, + { ...exampleOperation, isBucketed: true, dataType: 'date' }, + ]; + const ops: Operation[] = [ + ...bucketedOps, + { ...exampleOperation, dataType: 'number' }, + { ...exampleOperation, dataType: 'string' }, + { ...exampleOperation, dataType: 'boolean' }, + { ...exampleOperation, dataType: 'date' }, + ]; + expect(ops.filter(filterOperations)).toEqual(bucketedOps); + }); + + it('should not allow anything to be added to x', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + expect(options.find(o => o.groupId === 'x')?.supportsMoreColumns).toBe(false); + }); + + it('should allow number operations on y', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + const filterOperations = options.find(o => o.groupId === 'y')!.filterOperations; + const exampleOperation: Operation = { + dataType: 'number', + isBucketed: false, + label: 'bar', + }; + const ops: Operation[] = [ + { ...exampleOperation, dataType: 'number' }, + { ...exampleOperation, dataType: 'string' }, + { ...exampleOperation, dataType: 'boolean' }, + { ...exampleOperation, dataType: 'date' }, + ]; + expect(ops.filter(filterOperations).map(x => x.dataType)).toEqual(['number']); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx index 75d6fcc7d160b..c72fa0fec24d7 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -11,17 +11,18 @@ import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; -import { XYConfigPanel, LayerContextMenu } from './xy_config_panel'; -import { Visualization } from '../types'; +import { LayerContextMenu } from './xy_config_panel'; +import { Visualization, OperationMetadata } from '../types'; import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; import { toExpression, toPreviewExpression } from './to_expression'; -import { generateId } from '../id_generator'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; import chartMixedSVG from '../assets/chart_mixed_xy.svg'; import { isHorizontalChart } from './state_helpers'; const defaultIcon = chartBarStackedSVG; const defaultSeriesType = 'bar_stacked'; +const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; +const isBucketed = (op: OperationMetadata) => op.isBucketed; function getDescription(state?: State) { if (!state) { @@ -133,12 +134,10 @@ export const xyVisualization: Visualization = { layers: [ { layerId: frame.addNewLayer(), - accessors: [generateId()], + accessors: [], position: Position.Top, seriesType: defaultSeriesType, showGridlines: false, - splitAccessor: generateId(), - xAccessor: generateId(), }, ], } @@ -147,13 +146,89 @@ export const xyVisualization: Visualization = { getPersistableState: state => state, - renderLayerConfigPanel: (domElement, props) => - render( - - - , - domElement - ), + getConfiguration(props) { + const layer = props.state.layers.find(l => l.layerId === props.layerId)!; + return { + groups: [ + { + groupId: 'x', + groupLabel: i18n.translate('xpack.lens.xyChart.xAxisLabel', { + defaultMessage: 'X-axis', + }), + accessors: layer.xAccessor ? [layer.xAccessor] : [], + filterOperations: isBucketed, + suggestedPriority: 1, + supportsMoreColumns: !layer.xAccessor, + required: true, + dataTestSubj: 'lnsXY_xDimensionPanel', + }, + { + groupId: 'y', + groupLabel: i18n.translate('xpack.lens.xyChart.yAxisLabel', { + defaultMessage: 'Y-axis', + }), + accessors: layer.accessors, + filterOperations: isNumericMetric, + supportsMoreColumns: true, + required: true, + dataTestSubj: 'lnsXY_yDimensionPanel', + }, + { + groupId: 'breakdown', + groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { + defaultMessage: 'Break down by', + }), + accessors: layer.splitAccessor ? [layer.splitAccessor] : [], + filterOperations: isBucketed, + suggestedPriority: 0, + supportsMoreColumns: !layer.splitAccessor, + dataTestSubj: 'lnsXY_splitDimensionPanel', + }, + ], + }; + }, + + setDimension({ prevState, layerId, columnId, groupId }) { + const newLayer = prevState.layers.find(l => l.layerId === layerId); + if (!newLayer) { + return prevState; + } + + if (groupId === 'x') { + newLayer.xAccessor = columnId; + } + if (groupId === 'y') { + newLayer.accessors = [...newLayer.accessors.filter(a => a !== columnId), columnId]; + } + if (groupId === 'breakdown') { + newLayer.splitAccessor = columnId; + } + + return { + ...prevState, + layers: prevState.layers.map(l => (l.layerId === layerId ? newLayer : l)), + }; + }, + + removeDimension({ prevState, layerId, columnId }) { + const newLayer = prevState.layers.find(l => l.layerId === layerId); + if (!newLayer) { + return prevState; + } + + if (newLayer.xAccessor === columnId) { + delete newLayer.xAccessor; + } else if (newLayer.splitAccessor === columnId) { + delete newLayer.splitAccessor; + } else if (newLayer.accessors.includes(columnId)) { + newLayer.accessors = newLayer.accessors.filter(a => a !== columnId); + } + + return { + ...prevState, + layers: prevState.layers.map(l => (l.layerId === layerId ? newLayer : l)), + }; + }, getLayerContextMenuIcon({ state, layerId }) { const layer = state.layers.find(l => l.layerId === layerId); @@ -177,8 +252,6 @@ function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig { return { layerId, seriesType, - xAccessor: generateId(), - accessors: [generateId()], - splitAccessor: generateId(), + accessors: [], }; } diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap deleted file mode 100644 index e19958568b3be..0000000000000 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ /dev/null @@ -1,2826 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UploadLicense should display a modal when license requires acknowledgement 1`] = ` - - - - - -
- -
- -

- - Upload your license - -

-
- -
- - - -
-
-
-
- -
-
-
- Confirm License Upload -
-
-
-
-
-
-
- Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
-
-
    -
  • - Watcher will be disabled -
  • -
-
-
-
-
-
-
- - -
-
-
-
-
-
- } - > - - } - confirmButtonText={ - - } - onCancel={[Function]} - onConfirm={[Function]} - title={ - - } - > - - - -
-
-
- -
- -
-
-
- Confirm License Upload -
-
-
-
-
-
-
- Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
-
-
    -
  • - Watcher will be disabled -
  • -
-
-
-
-
-
-
- - -
-
-
-
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - > - -
- -
-
-
- Confirm License Upload -
-
-
-
-
-
-
- Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
-
-
    -
  • - Watcher will be disabled -
  • -
-
-
-
-
-
-
- - -
-
-
-
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - /> - -
- - - - - -
- -
- -
- - Confirm License Upload - -
-
-
-
- -
-
- -
-
- -
- Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
-
- -
-
    -
  • - Watcher will be disabled -
  • -
-
-
-
-
-
-
-
-
- -
- - - - - - -
-
-
-
-
-
- - - - - - - -
-

- - Your license key is a JSON file with a signature attached. - -

-

- - - , - } - } - > - Uploading a license will replace your current - - license. - -

-
-
- -
- - -
- -
- -
- -
- - } - onChange={[Function]} - > - -
-
- - - -
-
-
- - -
- -
- -
- - -
- - -
- - -
- - - - -
- - - -
-
-
-
-
- -
- -
- - - - - -`; - -exports[`UploadLicense should display an error when ES says license is expired 1`] = ` - - - - - -
- -
- -

- - Upload your license - -

-
- -
- - -
-

- - Your license key is a JSON file with a signature attached. - -

-

- - - , - } - } - > - Uploading a license will replace your current - - license. - -

-
- - -
- - -
- - -
-
- - Please address the errors in your form. - -
- -
-
    -
  • - The supplied license has expired. -
  • -
-
-
-
-
-
- -
- -
- -
- - } - onChange={[Function]} - > - -
-
- - - -
-
-
- - -
- -
- -
- - -
- - -
- - -
- - - - -
- - - -
-
-
-
-
- -
- -
- - - - - -`; - -exports[`UploadLicense should display an error when ES says license is invalid 1`] = ` - - - - - -
- -
- -

- - Upload your license - -

-
- -
- - -
-

- - Your license key is a JSON file with a signature attached. - -

-

- - - , - } - } - > - Uploading a license will replace your current - - license. - -

-
- - -
- - -
- - -
-
- - Please address the errors in your form. - -
- -
-
    -
  • - The supplied license is not valid for this product. -
  • -
-
-
-
-
-
- -
- -
- -
- - } - onChange={[Function]} - > - -
-
- - - -
-
-
- - -
- -
- -
- - -
- - -
- - -
- - - - -
- - - -
-
-
-
-
- -
- -
- - - - - -`; - -exports[`UploadLicense should display an error when submitting invalid JSON 1`] = ` - - - - - -
- -
- -

- - Upload your license - -

-
- -
- - -
-

- - Your license key is a JSON file with a signature attached. - -

-

- - - , - } - } - > - Uploading a license will replace your current - - license. - -

-
- - -
- - -
- - -
-
- - Please address the errors in your form. - -
- -
-
    -
  • - Error encountered uploading license: Check your license file. -
  • -
-
-
-
-
-
- -
- -
- -
- - } - onChange={[Function]} - > - -
-
- - - -
-
-
- - -
- -
- -
- - -
- - -
- - -
- - - - -
- - - -
-
-
-
-
- -
- -
- - - - - -`; - -exports[`UploadLicense should display error when ES returns error 1`] = ` - - - - - -
- -
- -

- - Upload your license - -

-
- -
- - -
-

- - Your license key is a JSON file with a signature attached. - -

-

- - - , - } - } - > - Uploading a license will replace your current - - license. - -

-
- - -
- - -
- - -
-
- - Please address the errors in your form. - -
- -
-
    -
  • - Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled -
  • -
-
-
-
-
-
- -
- -
- -
- - } - onChange={[Function]} - > - -
-
- - - -
-
-
- - -
- -
- -
- - -
- - -
- - -
- - - - -
- - - -
-
-
-
-
- -
- -
- - - - - -`; diff --git a/x-pack/legacy/plugins/license_management/index.ts b/x-pack/legacy/plugins/license_management/index.ts deleted file mode 100644 index e9fbb56e9d6ac..0000000000000 --- a/x-pack/legacy/plugins/license_management/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Legacy } from 'kibana'; -import { resolve } from 'path'; -import { PLUGIN } from './common/constants'; -import { plugin } from './server/np_ready'; - -export function licenseManagement(kibana: any) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.license_management', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/np_ready/application/index.scss'), - managementSections: ['plugins/license_management/legacy'], - injectDefaultVars(server: Legacy.Server) { - const config = server.config(); - return { - licenseManagementUiEnabled: config.get('xpack.license_management.ui.enabled'), - }; - }, - }, - config(Joi: any) { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - }).default(); - }, - init: (server: Legacy.Server) => { - plugin({} as any).setup(server.newPlatform.setup.core, { - ...server.newPlatform.setup.plugins, - __LEGACY: { - xpackMain: server.plugins.xpack_main, - elasticsearch: server.plugins.elasticsearch, - }, - }); - }, - }); -} diff --git a/x-pack/legacy/plugins/license_management/public/management_section.ts b/x-pack/legacy/plugins/license_management/public/management_section.ts deleted file mode 100644 index c7232649857e3..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/management_section.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { management } from 'ui/management'; -import chrome from 'ui/chrome'; -import { BASE_PATH, PLUGIN } from '../common/constants'; - -const licenseManagementUiEnabled = chrome.getInjected('licenseManagementUiEnabled'); - -if (licenseManagementUiEnabled) { - management.getSection('elasticsearch').register('license_management', { - visible: true, - display: PLUGIN.TITLE, - order: 99, - url: `#${BASE_PATH}home`, - }); -} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx deleted file mode 100644 index 49bb4ce984e48..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { Provider } from 'react-redux'; -import { HashRouter } from 'react-router-dom'; -import { render, unmountComponentAtNode } from 'react-dom'; -import * as history from 'history'; -import { DocLinksStart, HttpSetup, ToastsSetup, ChromeStart } from 'src/core/public'; - -import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; -// @ts-ignore -import { App } from './app.container'; -// @ts-ignore -import { licenseManagementStore } from './store'; - -import { setDocLinks } from './lib/docs_links'; -import { BASE_PATH } from '../../../common/constants'; -import { Breadcrumb } from './breadcrumbs'; - -interface AppDependencies { - element: HTMLElement; - chrome: ChromeStart; - - I18nContext: any; - legacy: { - xpackInfo: any; - refreshXpack: () => void; - MANAGEMENT_BREADCRUMB: Breadcrumb; - }; - - toasts: ToastsSetup; - docLinks: DocLinksStart; - http: HttpSetup; - telemetry?: TelemetryPluginSetup; -} - -export const boot = (deps: AppDependencies) => { - const { I18nContext, element, legacy, toasts, docLinks, http, chrome, telemetry } = deps; - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; - const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; - const securityDocumentationLink = `${esBase}/security-settings.html`; - - const initialState = { license: legacy.xpackInfo.get('license') }; - - setDocLinks({ securityDocumentationLink }); - - const services = { - legacy: { - refreshXpack: legacy.refreshXpack, - xPackInfo: legacy.xpackInfo, - }, - // So we can imperatively control the hash route - history: history.createHashHistory({ basename: BASE_PATH }), - toasts, - http, - chrome, - telemetry, - MANAGEMENT_BREADCRUMB: legacy.MANAGEMENT_BREADCRUMB, - }; - - const store = licenseManagementStore(initialState, services); - - render( - - - - - - - , - element - ); - - return () => unmountComponentAtNode(element); -}; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/breadcrumbs.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/breadcrumbs.ts deleted file mode 100644 index 2da04b22c0386..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/breadcrumbs.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -import { BASE_PATH } from '../../../common/constants'; - -export interface Breadcrumb { - text: string; - href: string; -} - -export function getDashboardBreadcrumbs(root: Breadcrumb) { - return [ - root, - { - text: i18n.translate('xpack.licenseMgmt.dashboard.breadcrumb', { - defaultMessage: 'License management', - }), - href: `#${BASE_PATH}home`, - }, - ]; -} - -export function getUploadBreadcrumbs(root: Breadcrumb) { - return [ - ...getDashboardBreadcrumbs(root), - { - text: i18n.translate('xpack.licenseMgmt.upload.breadcrumb', { - defaultMessage: 'Upload', - }), - }, - ]; -} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/set_breadcrumb.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/set_breadcrumb.ts deleted file mode 100644 index bcb4a907bdf88..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/set_breadcrumb.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { ThunkAction } from 'redux-thunk'; -import { ChromeStart } from 'src/core/public'; -import { getDashboardBreadcrumbs, getUploadBreadcrumbs, Breadcrumb } from '../../breadcrumbs'; - -export const setBreadcrumb = ( - section: 'dashboard' | 'upload' -): ThunkAction => ( - dispatch, - getState, - { chrome, MANAGEMENT_BREADCRUMB } -) => { - if (section === 'upload') { - chrome.setBreadcrumbs(getUploadBreadcrumbs(MANAGEMENT_BREADCRUMB)); - } else { - chrome.setBreadcrumbs(getDashboardBreadcrumbs(MANAGEMENT_BREADCRUMB)); - } -}; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts deleted file mode 100644 index 60876c9b638d1..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; -import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; -import { PLUGIN } from '../../common/constants'; -import { Breadcrumb } from './application/breadcrumbs'; -export interface Plugins { - telemetry: TelemetryPluginSetup; - __LEGACY: { - xpackInfo: XPackMainPlugin; - refreshXpack: () => void; - MANAGEMENT_BREADCRUMB: Breadcrumb; - }; -} - -export class LicenseManagementUIPlugin implements Plugin { - setup({ application, notifications, http }: CoreSetup, { __LEGACY, telemetry }: Plugins) { - application.register({ - id: PLUGIN.ID, - title: PLUGIN.TITLE, - async mount( - { - core: { - docLinks, - i18n: { Context: I18nContext }, - chrome, - }, - }, - { element } - ) { - const { boot } = await import('./application'); - return boot({ - legacy: { ...__LEGACY }, - I18nContext, - toasts: notifications.toasts, - docLinks, - http, - element, - chrome, - telemetry, - }); - }, - }); - } - start(core: CoreStart, plugins: any) {} - stop() {} -} diff --git a/x-pack/legacy/plugins/license_management/public/register_route.ts b/x-pack/legacy/plugins/license_management/public/register_route.ts deleted file mode 100644 index f9258f68c555a..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/register_route.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { App } from 'src/core/public'; - -/* Legacy Imports */ -import { npSetup, npStart } from 'ui/new_platform'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; -import chrome from 'ui/chrome'; -import routes from 'ui/routes'; -// @ts-ignore -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import { plugin } from './np_ready'; -import { BASE_PATH } from '../common/constants'; - -const licenseManagementUiEnabled = chrome.getInjected('licenseManagementUiEnabled'); - -if (licenseManagementUiEnabled) { - /* - This method handles the cleanup needed when route is scope is destroyed. It also prevents Angular - from destroying scope when route changes and both old route and new route are this same route. - */ - const manageAngularLifecycle = ($scope: any, $route: any, unmount: () => void) => { - const lastRoute = $route.current; - const deregister = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - // if templates are the same we are on the same route - if (lastRoute.$$route.template === currentRoute.$$route.template) { - // this prevents angular from destroying scope - $route.current = lastRoute; - } - }); - $scope.$on('$destroy', () => { - if (deregister) { - deregister(); - } - unmount(); - }); - }; - - const template = ` -
-
`; - - routes.when(`${BASE_PATH}:view?`, { - template, - controllerAs: 'licenseManagement', - controller: class LicenseManagementController { - constructor($injector: any, $rootScope: any, $scope: any, $route: any) { - $scope.$$postDigest(() => { - const element = document.getElementById('licenseReactRoot')!; - - const refreshXpack = async () => { - await xpackInfo.refresh($injector); - }; - - plugin({} as any).setup( - { - ...npSetup.core, - application: { - ...npSetup.core.application, - async register(app: App) { - const unmountApp = await app.mount({ ...npStart } as any, { - element, - appBasePath: '', - onAppLeave: () => undefined, - // TODO: adapt to use Core's ScopedHistory - history: {} as any, - }); - manageAngularLifecycle($scope, $route, unmountApp as any); - }, - }, - }, - { - telemetry: (npSetup.plugins as any).telemetry, - __LEGACY: { xpackInfo, refreshXpack, MANAGEMENT_BREADCRUMB }, - } - ); - }); - } - } as any, - } as any); -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_trial.ts b/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_trial.ts deleted file mode 100644 index 3569085d413ca..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_trial.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { KibanaRequest } from 'src/core/server'; -import { ElasticsearchPlugin } from '../../../../../../../src/legacy/core_plugins/elasticsearch'; - -export async function canStartTrial( - req: KibanaRequest, - elasticsearch: ElasticsearchPlugin -) { - const { callWithRequest } = elasticsearch.getCluster('admin'); - const options = { - method: 'GET', - path: '/_license/trial_status', - }; - try { - const response = await callWithRequest(req as any, 'transport.request', options); - return response.eligible_to_start_trial; - } catch (error) { - return error.body; - } -} - -export async function startTrial( - req: KibanaRequest, - elasticsearch: ElasticsearchPlugin, - xpackInfo: any -) { - const { callWithRequest } = elasticsearch.getCluster('admin'); - const options = { - method: 'POST', - path: '/_license/start_trial?acknowledge=true', - }; - try { - const response = await callWithRequest(req as any, 'transport.request', options); - const { trial_was_started: trialWasStarted } = response; - if (trialWasStarted) { - await xpackInfo.refreshNow(); - } - return response; - } catch (error) { - return error.body; - } -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/server/np_ready/plugin.ts deleted file mode 100644 index 9f065cf98d715..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/plugin.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Plugin, CoreSetup } from 'src/core/server'; -import { Dependencies, Server } from './types'; - -import { - registerLicenseRoute, - registerStartTrialRoutes, - registerStartBasicRoute, - registerPermissionsRoute, -} from './routes/api/license'; - -export class LicenseManagementServerPlugin implements Plugin { - setup({ http }: CoreSetup, { __LEGACY }: Dependencies) { - const xpackInfo = __LEGACY.xpackMain.info; - const router = http.createRouter(); - - const server: Server = { - router, - }; - - const legacy = { plugins: __LEGACY }; - - registerLicenseRoute(server, legacy, xpackInfo); - registerStartTrialRoutes(server, legacy, xpackInfo); - registerStartBasicRoute(server, legacy, xpackInfo); - registerPermissionsRoute(server, legacy, xpackInfo); - } - start() {} - stop() {} -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts deleted file mode 100644 index cdc929a2f3bb3..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; -import { putLicense } from '../../../lib/license'; -import { Legacy, Server } from '../../../types'; - -export function registerLicenseRoute(server: Server, legacy: Legacy, xpackInfo: any) { - server.router.put( - { - path: '/api/license', - validate: { - query: schema.object({ acknowledge: schema.string() }), - body: schema.object({ - license: schema.object({}, { allowUnknowns: true }), - }), - }, - }, - async (ctx, request, response) => { - try { - return response.ok({ - body: await putLicense(request, legacy.plugins.elasticsearch, xpackInfo), - }); - } catch (e) { - return response.internalError({ body: e }); - } - } - ); -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_permissions_route.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_permissions_route.ts deleted file mode 100644 index 0f6c343d04fcd..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_permissions_route.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getPermissions } from '../../../lib/permissions'; -import { Legacy, Server } from '../../../types'; - -export function registerPermissionsRoute(server: Server, legacy: Legacy, xpackInfo: any) { - server.router.post( - { path: '/api/license/permissions', validate: false }, - async (ctx, request, response) => { - if (!xpackInfo) { - // xpackInfo is updated via poll, so it may not be available until polling has begun. - // In this rare situation, tell the client the service is temporarily unavailable. - return response.customError({ statusCode: 503, body: 'Security info unavailable' }); - } - - try { - return response.ok({ - body: await getPermissions(request, legacy.plugins.elasticsearch, xpackInfo), - }); - } catch (e) { - return response.internalError({ body: e }); - } - } - ); -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_basic_route.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_basic_route.ts deleted file mode 100644 index ee7ac8602104b..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_basic_route.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; -import { startBasic } from '../../../lib/start_basic'; -import { Legacy, Server } from '../../../types'; - -export function registerStartBasicRoute(server: Server, legacy: Legacy, xpackInfo: any) { - server.router.post( - { - path: '/api/license/start_basic', - validate: { query: schema.object({ acknowledge: schema.string() }) }, - }, - async (ctx, request, response) => { - try { - return response.ok({ - body: await startBasic(request, legacy.plugins.elasticsearch, xpackInfo), - }); - } catch (e) { - return response.internalError({ body: e }); - } - } - ); -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_trial_routes.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_trial_routes.ts deleted file mode 100644 index d93f13eba363a..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_trial_routes.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { canStartTrial, startTrial } from '../../../lib/start_trial'; -import { Legacy, Server } from '../../../types'; - -export function registerStartTrialRoutes(server: Server, legacy: Legacy, xpackInfo: any) { - server.router.get( - { path: '/api/license/start_trial', validate: false }, - async (ctx, request, response) => { - try { - return response.ok({ body: await canStartTrial(request, legacy.plugins.elasticsearch) }); - } catch (e) { - return response.internalError({ body: e }); - } - } - ); - - server.router.post( - { path: '/api/license/start_trial', validate: false }, - async (ctx, request, response) => { - try { - return response.ok({ - body: await startTrial(request, legacy.plugins.elasticsearch, xpackInfo), - }); - } catch (e) { - return response.internalError({ body: e }); - } - } - ); -} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/types.ts b/x-pack/legacy/plugins/license_management/server/np_ready/types.ts deleted file mode 100644 index 0e66946ec1cc6..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/np_ready/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { IRouter } from 'src/core/server'; -import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; -import { ElasticsearchPlugin } from '../../../../../../src/legacy/core_plugins/elasticsearch'; - -export interface Dependencies { - __LEGACY: { - xpackMain: XPackMainPlugin; - elasticsearch: ElasticsearchPlugin; - }; -} - -export interface Server { - router: IRouter; -} - -export interface Legacy { - plugins: Dependencies['__LEGACY']; -} diff --git a/x-pack/legacy/plugins/maps/common/data_request_descriptor_types.d.ts b/x-pack/legacy/plugins/maps/common/data_request_descriptor_types.d.ts new file mode 100644 index 0000000000000..3281fb5892eac --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/data_request_descriptor_types.d.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +// Global map state passed to every layer. +export type MapFilters = { + buffer: unknown; + extent: unknown; + filters: unknown[]; + query: unknown; + refreshTimerLastTriggeredAt: string; + timeFilters: unknown; + zoom: number; +}; + +export type VectorLayerRequestMeta = MapFilters & { + applyGlobalQuery: boolean; + fieldNames: string[]; + geogridPrecision: number; + sourceQuery: unknown; + sourceMeta: unknown; +}; + +export type ESSearchSourceResponseMeta = { + areResultsTrimmed?: boolean; + sourceType?: string; + + // top hits meta + areEntitiesTrimmed?: boolean; + entityCount?: number; + totalEntities?: number; +}; + +// Partial because objects are justified downstream in constructors +export type DataMeta = Partial & Partial; + +export type DataRequestDescriptor = { + dataId: string; + dataMetaAtStart?: DataMeta; + dataRequestToken?: symbol; + data?: object; + dataMeta?: DataMeta; +}; diff --git a/x-pack/legacy/plugins/maps/common/descriptor_types.d.ts b/x-pack/legacy/plugins/maps/common/descriptor_types.d.ts index ce0743ba2baed..2f45c525828db 100644 --- a/x-pack/legacy/plugins/maps/common/descriptor_types.d.ts +++ b/x-pack/legacy/plugins/maps/common/descriptor_types.d.ts @@ -5,7 +5,8 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER } from './constants'; +import { DataRequestDescriptor } from './data_request_descriptor_types'; +import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from './constants'; export type AbstractSourceDescriptor = { id?: string; @@ -49,7 +50,7 @@ export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & { tooltipProperties?: string[]; sortField?: string; sortOrder?: SORT_ORDER; - useTopHits?: boolean; + scalingType: SCALING_TYPES; topHitsSplitField?: string; topHitsSize?: number; }; @@ -93,14 +94,6 @@ export type JoinDescriptor = { right: ESTermSourceDescriptor; }; -export type DataRequestDescriptor = { - dataId: string; - dataMetaAtStart: object; - dataRequestToken: symbol; - data: object; - dataMeta: object; -}; - export type LayerDescriptor = { __dataRequests?: DataRequestDescriptor[]; __isInErrorState?: boolean; diff --git a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts b/x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts new file mode 100644 index 0000000000000..4fbb1ef4c55ed --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { migrateUseTopHitsToScalingType } from './scaling_type'; + +describe('migrateUseTopHitsToScalingType', () => { + test('Should handle missing layerListJSON attribute', () => { + const attributes = { + title: 'my map', + }; + expect(migrateUseTopHitsToScalingType({ attributes })).toEqual({ + title: 'my map', + }); + }); + + test('Should migrate useTopHits: true to scalingType TOP_HITS for ES documents sources', () => { + const layerListJSON = JSON.stringify([ + { + sourceDescriptor: { + type: 'ES_SEARCH', + useTopHits: true, + }, + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + expect(migrateUseTopHitsToScalingType({ attributes })).toEqual({ + title: 'my map', + layerListJSON: '[{"sourceDescriptor":{"type":"ES_SEARCH","scalingType":"TOP_HITS"}}]', + }); + }); + + test('Should migrate useTopHits: false to scalingType LIMIT for ES documents sources', () => { + const layerListJSON = JSON.stringify([ + { + sourceDescriptor: { + type: 'ES_SEARCH', + useTopHits: false, + }, + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + expect(migrateUseTopHitsToScalingType({ attributes })).toEqual({ + title: 'my map', + layerListJSON: '[{"sourceDescriptor":{"type":"ES_SEARCH","scalingType":"LIMIT"}}]', + }); + }); + + test('Should set scalingType to LIMIT when useTopHits is not set', () => { + const layerListJSON = JSON.stringify([ + { + sourceDescriptor: { + type: 'ES_SEARCH', + }, + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + expect(migrateUseTopHitsToScalingType({ attributes })).toEqual({ + title: 'my map', + layerListJSON: '[{"sourceDescriptor":{"type":"ES_SEARCH","scalingType":"LIMIT"}}]', + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts b/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts new file mode 100644 index 0000000000000..5823ddd6b42e3 --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { ES_SEARCH, SCALING_TYPES } from '../constants'; +import { LayerDescriptor, ESSearchSourceDescriptor } from '../descriptor_types'; +import { MapSavedObjectAttributes } from '../../../../../plugins/maps/common/map_saved_object_type'; + +function isEsDocumentSource(layerDescriptor: LayerDescriptor) { + const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); + return sourceType === ES_SEARCH; +} + +export function migrateUseTopHitsToScalingType({ + attributes, +}: { + attributes: MapSavedObjectAttributes; +}): MapSavedObjectAttributes { + if (!attributes || !attributes.layerListJSON) { + return attributes; + } + + const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + layerList.forEach((layerDescriptor: LayerDescriptor) => { + if (isEsDocumentSource(layerDescriptor)) { + const sourceDescriptor = layerDescriptor.sourceDescriptor as ESSearchSourceDescriptor; + sourceDescriptor.scalingType = _.get(layerDescriptor, 'sourceDescriptor.useTopHits', false) + ? SCALING_TYPES.TOP_HITS + : SCALING_TYPES.LIMIT; + // @ts-ignore useTopHits no longer in type definition but that does not mean its not in live data + // hence the entire point of this method + delete sourceDescriptor.useTopHits; + } + }); + + return { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }; +} diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/legacy/plugins/maps/migrations.js index 9622f6ba63fac..6a1f5bc937497 100644 --- a/x-pack/legacy/plugins/maps/migrations.js +++ b/x-pack/legacy/plugins/maps/migrations.js @@ -10,6 +10,7 @@ import { topHitsTimeToSort } from './common/migrations/top_hits_time_to_sort'; import { moveApplyGlobalQueryToSources } from './common/migrations/move_apply_global_query'; import { addFieldMetaOptions } from './common/migrations/add_field_meta_options'; import { migrateSymbolStyleDescriptor } from './common/migrations/migrate_symbol_style_descriptor'; +import { migrateUseTopHitsToScalingType } from './common/migrations/scaling_type'; export const migrations = { map: { @@ -48,11 +49,12 @@ export const migrations = { }; }, '7.7.0': doc => { - const attributes = migrateSymbolStyleDescriptor(doc); + const attributesPhase1 = migrateSymbolStyleDescriptor(doc); + const attributesPhase2 = migrateUseTopHitsToScalingType({ attributes: attributesPhase1 }); return { ...doc, - attributes, + attributes: attributesPhase2, }; }, }, diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts b/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts new file mode 100644 index 0000000000000..418f2880c1077 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { DataMeta, MapFilters } from '../../common/data_request_descriptor_types'; + +export type SyncContext = { + startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void; + stopLoading(dataId: string, requestToken: symbol, data: unknown, meta: DataMeta): void; + onLoadError(dataId: string, requestToken: symbol, errorMessage: string): void; + updateSourceData(newData: unknown): void; + isRequestStillActive(dataId: string, requestToken: symbol): boolean; + registerCancelCallback(requestToken: symbol, callback: () => void): void; + dataFilters: MapFilters; +}; diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js index 7a1e5e5266246..415630d9f730b 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js @@ -649,13 +649,14 @@ export function onDataLoadError(layerId, dataId, requestToken, errorMessage) { }; } -export function updateSourceProp(layerId, propName, value) { +export function updateSourceProp(layerId, propName, value, newLayerType) { return async dispatch => { dispatch({ type: UPDATE_SOURCE_PROP, layerId, propName, value, + newLayerType, }); await dispatch(clearMissingStyleProperties(layerId)); dispatch(syncDataForLayer(layerId)); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 94e855fc6708f..60bbaa9825db7 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -16,8 +16,6 @@ import { EuiTextColor, EuiTextAlign, EuiButtonEmpty, - EuiFormRow, - EuiSwitch, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -80,14 +78,6 @@ export class FilterEditor extends Component { this._close(); }; - _onFilterByMapBoundsChange = event => { - this.props.updateSourceProp( - this.props.layer.getId(), - 'filterByMapBounds', - event.target.checked - ); - }; - _onApplyGlobalQueryChange = applyGlobalQuery => { this.props.updateSourceProp(this.props.layer.getId(), 'applyGlobalQuery', applyGlobalQuery); }; @@ -182,22 +172,6 @@ export class FilterEditor extends Component { } render() { - let filterByBoundsSwitch; - if (this.props.layer.getSource().isFilterByMapBoundsConfigurable()) { - filterByBoundsSwitch = ( - - - - ); - } - return ( @@ -217,8 +191,6 @@ export class FilterEditor extends Component { - {filterByBoundsSwitch} - { dispatch(fitToLayerExtent(layerId)); }, - updateSourceProp: (id, propName, value) => dispatch(updateSourceProp(id, propName, value)), + updateSourceProp: (id, propName, value, newLayerType) => + dispatch(updateSourceProp(id, propName, value, newLayerType)), }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js index 755d4bb6b323a..1b269e388bea0 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js @@ -99,8 +99,8 @@ export class LayerPanel extends React.Component { } } - _onSourceChange = ({ propName, value }) => { - this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value); + _onSourceChange = ({ propName, value, newLayerType }) => { + this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); }; _renderFilterSection() { diff --git a/x-pack/legacy/plugins/maps/public/layers/blended_vector_layer.ts b/x-pack/legacy/plugins/maps/public/layers/blended_vector_layer.ts new file mode 100644 index 0000000000000..b35eeedfa44fa --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/blended_vector_layer.ts @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { VectorLayer } from './vector_layer'; +import { IVectorStyle, VectorStyle } from './styles/vector/vector_style'; +// @ts-ignore +import { getDefaultDynamicProperties, VECTOR_STYLES } from './styles/vector/vector_style_defaults'; +import { IDynamicStyleProperty } from './styles/vector/properties/dynamic_style_property'; +import { IStyleProperty } from './styles/vector/properties/style_property'; +import { + COUNT_PROP_LABEL, + COUNT_PROP_NAME, + ES_GEO_GRID, + LAYER_TYPE, + AGG_TYPE, + SOURCE_DATA_ID_ORIGIN, + RENDER_AS, + STYLE_TYPE, +} from '../../common/constants'; +import { ESGeoGridSource } from './sources/es_geo_grid_source/es_geo_grid_source'; +// @ts-ignore +import { canSkipSourceUpdate } from './util/can_skip_fetch'; +import { IVectorLayer, VectorLayerArguments } from './vector_layer'; +import { IESSource } from './sources/es_source'; +import { IESAggSource } from './sources/es_agg_source'; +import { ISource } from './sources/source'; +import { SyncContext } from '../actions/map_actions'; +import { DataRequestAbortError } from './util/data_request'; + +const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; + +function getAggType(dynamicProperty: IDynamicStyleProperty): AGG_TYPE { + return dynamicProperty.isOrdinal() ? AGG_TYPE.AVG : AGG_TYPE.TERMS; +} + +function getClusterSource(documentSource: IESSource, documentStyle: IVectorStyle): IESAggSource { + const clusterSourceDescriptor = ESGeoGridSource.createDescriptor({ + indexPatternId: documentSource.getIndexPatternId(), + geoField: documentSource.getGeoFieldName(), + requestType: RENDER_AS.POINT, + }); + clusterSourceDescriptor.metrics = [ + { + type: AGG_TYPE.COUNT, + label: COUNT_PROP_LABEL, + }, + ...documentStyle.getDynamicPropertiesArray().map(dynamicProperty => { + return { + type: getAggType(dynamicProperty), + field: dynamicProperty.getFieldName(), + }; + }), + ]; + clusterSourceDescriptor.id = documentSource.getId(); + return new ESGeoGridSource(clusterSourceDescriptor, documentSource.getInspectorAdapters()); +} + +function getClusterStyleDescriptor( + documentStyle: IVectorStyle, + clusterSource: IESAggSource +): unknown { + const defaultDynamicProperties = getDefaultDynamicProperties(); + const clusterStyleDescriptor: any = { + ...documentStyle.getDescriptor(), + properties: { + [VECTOR_STYLES.LABEL_TEXT]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options, + field: { + name: COUNT_PROP_NAME, + origin: SOURCE_DATA_ID_ORIGIN, + }, + }, + }, + [VECTOR_STYLES.ICON_SIZE]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options, + field: { + name: COUNT_PROP_NAME, + origin: SOURCE_DATA_ID_ORIGIN, + }, + }, + }, + }, + }; + documentStyle.getAllStyleProperties().forEach((styleProperty: IStyleProperty) => { + const styleName = styleProperty.getStyleName(); + if ( + [VECTOR_STYLES.LABEL_TEXT, VECTOR_STYLES.ICON_SIZE].includes(styleName) && + (!styleProperty.isDynamic() || !styleProperty.isComplete()) + ) { + // Do not migrate static label and icon size properties to provide unique cluster styling out of the box + return; + } + + if (styleProperty.isDynamic()) { + const options = (styleProperty as IDynamicStyleProperty).getOptions(); + const field = + options && options.field && options.field.name + ? { + ...options.field, + name: clusterSource.getAggKey( + getAggType(styleProperty as IDynamicStyleProperty), + options.field.name + ), + } + : undefined; + clusterStyleDescriptor.properties[styleName] = { + type: STYLE_TYPE.DYNAMIC, + options: { + ...options, + field, + }, + }; + } else { + clusterStyleDescriptor.properties[styleName] = { + type: STYLE_TYPE.STATIC, + options: { ...styleProperty.getOptions() }, + }; + } + }); + + return clusterStyleDescriptor; +} + +export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { + static type = LAYER_TYPE.BLENDED_VECTOR; + + static createDescriptor(options: VectorLayerArguments, mapColors: string[]) { + const layerDescriptor = VectorLayer.createDescriptor(options, mapColors); + layerDescriptor.type = BlendedVectorLayer.type; + return layerDescriptor; + } + + private readonly _isClustered: boolean; + private readonly _clusterSource: IESAggSource; + private readonly _clusterStyle: IVectorStyle; + private readonly _documentSource: IESSource; + private readonly _documentStyle: IVectorStyle; + + constructor(options: VectorLayerArguments) { + super(options); + + this._documentSource = this._source as IESSource; // VectorLayer constructor sets _source as document source + this._documentStyle = this._style; // VectorLayer constructor sets _style as document source + + this._clusterSource = getClusterSource(this._documentSource, this._documentStyle); + const clusterStyleDescriptor = getClusterStyleDescriptor( + this._documentStyle, + this._clusterSource + ); + this._clusterStyle = new VectorStyle(clusterStyleDescriptor, this._clusterSource, this); + + let isClustered = false; + const sourceDataRequest = this.getSourceDataRequest(); + if (sourceDataRequest) { + const requestMeta = sourceDataRequest.getMeta(); + if (requestMeta && requestMeta.sourceType && requestMeta.sourceType === ES_GEO_GRID) { + isClustered = true; + } + } + this._isClustered = isClustered; + } + + destroy() { + if (this._documentSource) { + this._documentSource.destroy(); + } + if (this._clusterSource) { + this._clusterSource.destroy(); + } + } + + async getDisplayName(source: ISource) { + const displayName = await super.getDisplayName(source); + return this._isClustered + ? i18n.translate('xpack.maps.blendedVectorLayer.clusteredLayerName', { + defaultMessage: 'Clustered {displayName}', + values: { displayName }, + }) + : displayName; + } + + isJoinable() { + return false; + } + + getJoins() { + return []; + } + + getSource() { + return this._isClustered ? this._clusterSource : this._documentSource; + } + + getSourceForEditing() { + // Layer is based on this._documentSource + // this._clusterSource is a derived source for rendering only. + // Regardless of this._activeSource, this._documentSource should always be displayed in the editor + return this._documentSource; + } + + getCurrentStyle() { + return this._isClustered ? this._clusterStyle : this._documentStyle; + } + + getStyleForEditing() { + return this._documentStyle; + } + + async syncData(syncContext: SyncContext) { + const dataRequestId = ACTIVE_COUNT_DATA_ID; + const requestToken = Symbol(`layer-active-count:${this.getId()}`); + const searchFilters = this._getSearchFilters( + syncContext.dataFilters, + this.getSource(), + this.getCurrentStyle() + ); + const canSkipFetch = await canSkipSourceUpdate({ + source: this.getSource(), + prevDataRequest: this.getDataRequest(dataRequestId), + nextMeta: searchFilters, + }); + if (canSkipFetch) { + return; + } + + let isSyncClustered; + try { + syncContext.startLoading(dataRequestId, requestToken, searchFilters); + const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0); + const resp = await searchSource.fetch(); + const maxResultWindow = await this._documentSource.getMaxResultWindow(); + isSyncClustered = resp.hits.total > maxResultWindow; + syncContext.stopLoading(dataRequestId, requestToken, { isSyncClustered }, searchFilters); + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + syncContext.onLoadError(dataRequestId, requestToken, error.message); + } + return; + } + + let activeSource; + let activeStyle; + if (isSyncClustered) { + activeSource = this._clusterSource; + activeStyle = this._clusterStyle; + } else { + activeSource = this._documentSource; + activeStyle = this._documentStyle; + } + + super._syncData(syncContext, activeSource, activeStyle); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.ts b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.ts index c9dfdf6ad1fef..65f952ca01038 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.ts +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.ts @@ -23,11 +23,11 @@ export interface IESAggField extends IField { } export class ESAggField implements IESAggField { - private _source: IESAggSource; - private _origin: FIELD_ORIGIN; - private _label?: string; - private _aggType: AGG_TYPE; - private _esDocField?: IField | undefined; + private readonly _source: IESAggSource; + private readonly _origin: FIELD_ORIGIN; + private readonly _label?: string; + private readonly _aggType: AGG_TYPE; + private readonly _esDocField?: IField | undefined; constructor({ label, diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/top_term_percentage_field.ts b/x-pack/legacy/plugins/maps/public/layers/fields/top_term_percentage_field.ts index bb40a24288a28..84bade4d94490 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/top_term_percentage_field.ts +++ b/x-pack/legacy/plugins/maps/public/layers/fields/top_term_percentage_field.ts @@ -12,7 +12,7 @@ import { TOP_TERM_PERCENTAGE_SUFFIX } from '../../../common/constants'; import { FIELD_ORIGIN } from '../../../common/constants'; export class TopTermPercentageField implements IESAggField { - private _topTermAggField: IESAggField; + private readonly _topTermAggField: IESAggField; constructor(topTermAggField: IESAggField) { this._topTermAggField = topTermAggField; diff --git a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js index 29223d6a67c6b..ef78b5afe3a3a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js @@ -32,7 +32,7 @@ export class HeatmapLayer extends VectorLayer { } _getPropKeyOfSelectedMetric() { - const metricfields = this._source.getMetricFields(); + const metricfields = this.getSource().getMetricFields(); return metricfields[0].getName(); } @@ -84,11 +84,11 @@ export class HeatmapLayer extends VectorLayer { } this.syncVisibilityWithMb(mbMap, heatmapLayerId); - this._style.setMBPaintProperties({ + this.getCurrentStyle().setMBPaintProperties({ mbMap, layerId: heatmapLayerId, propertyName: SCALED_PROPERTY_NAME, - resolution: this._source.getGridResolution(), + resolution: this.getSource().getGridResolution(), }); mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha()); mbMap.setLayerZoomRange(heatmapLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); @@ -103,7 +103,7 @@ export class HeatmapLayer extends VectorLayer { } renderLegendDetails() { - const metricFields = this._source.getMetricFields(); - return this._style.renderLegendDetails(metricFields[0]); + const metricFields = this.getSource().getMetricFields(); + return this.getCurrentStyle().renderLegendDetails(metricFields[0]); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.d.ts b/x-pack/legacy/plugins/maps/public/layers/layer.d.ts index eebbaac7d4f97..777566298e607 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/layer.d.ts @@ -5,9 +5,17 @@ */ import { LayerDescriptor } from '../../common/descriptor_types'; import { ISource } from './sources/source'; +import { DataRequest } from './util/data_request'; +import { SyncContext } from '../actions/map_actions'; export interface ILayer { - getDisplayName(): Promise; + getDataRequest(id: string): DataRequest | undefined; + getDisplayName(source?: ISource): Promise; + getId(): string; + getSourceDataRequest(): DataRequest | undefined; + getSource(): ISource; + getSourceForEditing(): ISource; + syncData(syncContext: SyncContext): Promise; } export interface ILayerArguments { @@ -17,5 +25,11 @@ export interface ILayerArguments { export class AbstractLayer implements ILayer { constructor(layerArguments: ILayerArguments); - getDisplayName(): Promise; + getDataRequest(id: string): DataRequest | undefined; + getDisplayName(source?: ISource): Promise; + getId(): string; + getSourceDataRequest(): DataRequest | undefined; + getSource(): ISource; + getSourceForEditing(): ISource; + syncData(syncContext: SyncContext): Promise; } diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 5c9532a3841f3..d162e342dfd1a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -63,7 +63,7 @@ export class AbstractLayer { clonedDescriptor.id = uuid(); const displayName = await this.getDisplayName(); clonedDescriptor.label = `Clone of ${displayName}`; - clonedDescriptor.sourceDescriptor = this._source.cloneDescriptor(); + clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); if (clonedDescriptor.joins) { clonedDescriptor.joins.forEach(joinDescriptor => { // right.id is uuid used to track requests in inspector @@ -78,28 +78,31 @@ export class AbstractLayer { } isJoinable() { - return this._source.isJoinable(); + return this.getSource().isJoinable(); } supportsElasticsearchFilters() { - return this._source.isESSource(); + return this.getSource().isESSource(); } async supportsFitToBounds() { - return await this._source.supportsFitToBounds(); + return await this.getSource().supportsFitToBounds(); } - async getDisplayName() { + async getDisplayName(source) { if (this._descriptor.label) { return this._descriptor.label; } - return (await this._source.getDisplayName()) || `Layer ${this._descriptor.id}`; + const sourceDisplayName = source + ? await source.getDisplayName() + : await this.getSource().getDisplayName(); + return sourceDisplayName || `Layer ${this._descriptor.id}`; } async getAttributions() { if (!this.hasErrors()) { - return await this._source.getAttributions(); + return await this.getSource().getAttributions(); } return []; } @@ -191,6 +194,10 @@ export class AbstractLayer { return this._source; } + getSourceForEditing() { + return this._source; + } + isVisible() { return this._descriptor.visible; } @@ -226,12 +233,16 @@ export class AbstractLayer { return this._style; } + getStyleForEditing() { + return this._style; + } + async getImmutableSourceProperties() { - return this._source.getImmutableProperties(); + return this.getSource().getImmutableProperties(); } renderSourceSettingsEditor = ({ onChange }) => { - return this._source.renderSourceSettingsEditor({ onChange }); + return this.getSourceForEditing().renderSourceSettingsEditor({ onChange }); }; getPrevRequestToken(dataId) { @@ -319,10 +330,11 @@ export class AbstractLayer { } renderStyleEditor({ onStyleDescriptorChange }) { - if (!this._style) { + const style = this.getStyleForEditing(); + if (!style) { return null; } - return this._style.renderEditor({ layer: this, onStyleDescriptorChange }); + return style.renderEditor({ layer: this, onStyleDescriptorChange }); } getIndexPatternIds() { @@ -333,10 +345,6 @@ export class AbstractLayer { return []; } - async getFields() { - return []; - } - syncVisibilityWithMb(mbMap, mbLayerId) { mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts index 48e90b6c41d51..3f596cea1ae39 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts @@ -6,10 +6,23 @@ import { AbstractESAggSource } from '../es_agg_source'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; -import { GRID_RESOLUTION } from '../../../../common/constants'; +import { GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants'; export class ESGeoGridSource extends AbstractESAggSource { + static createDescriptor({ + indexPatternId, + geoField, + requestType, + resolution, + }: { + indexPatternId: string; + geoField: string; + requestType: RENDER_AS; + resolution?: GRID_RESOLUTION; + }): ESGeoGridSourceDescriptor; + constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown); + getGridResolution(): GRID_RESOLUTION; getGeoGridPrecision(zoom: number): number; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index 3b3e8004ded05..5ad202a02ae6d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -75,7 +75,7 @@ export class ESGeoGridSource extends AbstractESAggSource { renderSourceSettingsEditor({ onChange }) { return ( { @@ -325,6 +325,7 @@ export class ESGeoGridSource extends AbstractESAggSource { }, meta: { areResultsTrimmed: false, + sourceType: ES_GEO_GRID, }, }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index 53536b11aaca6..8e1145c531f9e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -64,7 +64,7 @@ export class ESPewPewSource extends AbstractESAggSource { renderSourceSettingsEditor({ onChange }) { return ( - + + + +
+ +
+
+ + + + @@ -112,7 +152,7 @@ exports[`should enable sort order select when sort field provided 1`] = `
`; -exports[`should render top hits form when useTopHits is true 1`] = ` +exports[`should render top hits form when scaling type is TOP_HITS 1`] = ` - + + + +
+ +
+
+ + + + + - + + + +
+ +
+
+ + + + diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts index 5d8188f19f4ea..0a4e48a195ec6 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts @@ -8,5 +8,5 @@ import { AbstractESSource } from '../es_source'; import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; export class ESSearchSource extends AbstractESSource { - constructor(sourceDescriptor: ESSearchSourceDescriptor, inspectorAdapters: unknown); + constructor(sourceDescriptor: Partial, inspectorAdapters: unknown); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 7f0e870760512..440b9aa89a945 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -11,6 +11,8 @@ import uuid from 'uuid/v4'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { AbstractESSource } from '../es_source'; import { SearchSource } from '../../../kibana_services'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { VectorLayer } from '../../vector_layer'; import { hitsToGeoJson } from '../../../elasticsearch_geo_utils'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; @@ -19,11 +21,13 @@ import { ES_GEO_FIELD_TYPE, DEFAULT_MAX_BUCKETS_LIMIT, SORT_ORDER, + SCALING_TYPES, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { getSourceFields } from '../../../index_pattern_util'; import { loadIndexSettings } from './load_index_settings'; +import { BlendedVectorLayer } from '../../blended_vector_layer'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; @@ -99,7 +103,7 @@ export class ESSearchSource extends AbstractESSource { tooltipProperties: _.get(descriptor, 'tooltipProperties', []), sortField: _.get(descriptor, 'sortField', ''), sortOrder: _.get(descriptor, 'sortOrder', SORT_ORDER.DESC), - useTopHits: _.get(descriptor, 'useTopHits', false), + scalingType: _.get(descriptor, 'scalingType', SCALING_TYPES.LIMIT), topHitsSplitField: descriptor.topHitsSplitField, topHitsSize: _.get(descriptor, 'topHitsSize', 1), }, @@ -111,6 +115,32 @@ export class ESSearchSource extends AbstractESSource { ); } + createDefaultLayer(options, mapColors) { + if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) { + const layerDescriptor = BlendedVectorLayer.createDescriptor( + { + sourceDescriptor: this._descriptor, + ...options, + }, + mapColors + ); + const style = new VectorStyle(layerDescriptor.style, this); + return new BlendedVectorLayer({ + layerDescriptor: layerDescriptor, + source: this, + style, + }); + } + + const layerDescriptor = this._createDefaultLayerDescriptor(options, mapColors); + const style = new VectorStyle(layerDescriptor.style, this); + return new VectorLayer({ + layerDescriptor: layerDescriptor, + source: this, + style, + }); + } + createField({ fieldName }) { return new ESDocField({ fieldName, @@ -122,12 +152,14 @@ export class ESSearchSource extends AbstractESSource { return ( @@ -157,7 +189,7 @@ export class ESSearchSource extends AbstractESSource { } async getImmutableProperties() { - let indexPatternTitle = this._descriptor.indexPatternId; + let indexPatternTitle = this.getIndexPatternId(); let geoFieldType = ''; try { const indexPattern = await this.getIndexPattern(); @@ -239,7 +271,7 @@ export class ESSearchSource extends AbstractESSource { shard_size: DEFAULT_MAX_BUCKETS_LIMIT, }; - const searchSource = await this._makeSearchSource(searchFilters, 0); + const searchSource = await this.makeSearchSource(searchFilters, 0); searchSource.setField('aggs', { totalEntities: { cardinality: addFieldToDSL(cardinalityAgg, topHitsSplitField), @@ -300,7 +332,7 @@ export class ESSearchSource extends AbstractESSource { ); const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source - const searchSource = await this._makeSearchSource( + const searchSource = await this.makeSearchSource( searchFilters, maxResultWindow, initialSearchContext @@ -332,8 +364,8 @@ export class ESSearchSource extends AbstractESSource { } _isTopHits() { - const { useTopHits, topHitsSplitField } = this._descriptor; - return !!(useTopHits && topHitsSplitField); + const { scalingType, topHitsSplitField } = this._descriptor; + return !!(scalingType === SCALING_TYPES.TOP_HITS && topHitsSplitField); } _hasSort() { @@ -341,6 +373,12 @@ export class ESSearchSource extends AbstractESSource { return !!sortField && !!sortOrder; } + async getMaxResultWindow() { + const indexPattern = await this.getIndexPattern(); + const indexSettings = await loadIndexSettings(indexPattern.title); + return indexSettings.maxResultWindow; + } + async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { const indexPattern = await this.getIndexPattern(); @@ -383,7 +421,7 @@ export class ESSearchSource extends AbstractESSource { return { data: featureCollection, - meta, + meta: { ...meta, sourceType: ES_SEARCH }, }; } @@ -442,11 +480,9 @@ export class ESSearchSource extends AbstractESSource { } isFilterByMapBounds() { - return _.get(this._descriptor, 'filterByMapBounds', false); - } - - isFilterByMapBoundsConfigurable() { - return true; + return this._descriptor.scalingType === SCALING_TYPES.CLUSTER + ? true + : this._descriptor.filterByMapBounds; } async getLeftJoinFields() { @@ -533,7 +569,7 @@ export class ESSearchSource extends AbstractESSource { return { sortField: this._descriptor.sortField, sortOrder: this._descriptor.sortOrder, - useTopHits: this._descriptor.useTopHits, + scalingType: this._descriptor.scalingType, topHitsSplitField: this._descriptor.topHitsSplitField, topHitsSize: this._descriptor.topHitsSize, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts index 1e10923cea1d0..59120e221ca49 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts @@ -7,7 +7,7 @@ jest.mock('ui/new_platform'); import { ESSearchSource } from './es_search_source'; import { VectorLayer } from '../../vector_layer'; -import { ES_SEARCH } from '../../../../common/constants'; +import { ES_SEARCH, SCALING_TYPES } from '../../../../common/constants'; import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; const descriptor: ESSearchSourceDescriptor = { @@ -15,6 +15,7 @@ const descriptor: ESSearchSourceDescriptor = { id: '1234', indexPatternId: 'myIndexPattern', geoField: 'myLocation', + scalingType: SCALING_TYPES.LIMIT, }; describe('ES Search Source', () => { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js index 52702c1f4ecc7..b85cca113cf98 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js @@ -14,6 +14,7 @@ import { EuiPanel, EuiSpacer, EuiHorizontalRule, + EuiRadioGroup, } from '@elastic/eui'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { TooltipSelector } from '../../../components/tooltip_selector'; @@ -22,7 +23,13 @@ import { indexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; import { getTermsFields, getSourceFields } from '../../../index_pattern_util'; import { ValidatedRange } from '../../../components/validated_range'; -import { DEFAULT_MAX_INNER_RESULT_WINDOW, SORT_ORDER } from '../../../../common/constants'; +import { + DEFAULT_MAX_INNER_RESULT_WINDOW, + DEFAULT_MAX_RESULT_WINDOW, + SORT_ORDER, + SCALING_TYPES, + LAYER_TYPE, +} from '../../../../common/constants'; import { ESDocField } from '../../fields/es_doc_field'; import { FormattedMessage } from '@kbn/i18n/react'; import { loadIndexSettings } from './load_index_settings'; @@ -35,7 +42,7 @@ export class UpdateSourceEditor extends Component { tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired, sortField: PropTypes.string, sortOrder: PropTypes.string.isRequired, - useTopHits: PropTypes.bool.isRequired, + scalingType: PropTypes.string.isRequired, topHitsSplitField: PropTypes.string, topHitsSize: PropTypes.number.isRequired, source: PropTypes.object, @@ -46,6 +53,8 @@ export class UpdateSourceEditor extends Component { termFields: null, sortFields: null, maxInnerResultWindow: DEFAULT_MAX_INNER_RESULT_WINDOW, + maxResultWindow: DEFAULT_MAX_RESULT_WINDOW, + supportsClustering: false, }; componentDidMount() { @@ -61,9 +70,9 @@ export class UpdateSourceEditor extends Component { async loadIndexSettings() { try { const indexPattern = await indexPatternService.get(this.props.indexPatternId); - const { maxInnerResultWindow } = await loadIndexSettings(indexPattern.title); + const { maxInnerResultWindow, maxResultWindow } = await loadIndexSettings(indexPattern.title); if (this._isMounted) { - this.setState({ maxInnerResultWindow }); + this.setState({ maxInnerResultWindow, maxResultWindow }); } } catch (err) { return; @@ -88,6 +97,16 @@ export class UpdateSourceEditor extends Component { return; } + let geoField; + try { + geoField = await this.props.getGeoField(); + } catch (err) { + if (this._isMounted) { + this.setState({ loadError: err.message }); + } + return; + } + if (!this._isMounted) { return; } @@ -102,6 +121,7 @@ export class UpdateSourceEditor extends Component { }); this.setState({ + supportsClustering: geoField.aggregatable, sourceFields: sourceFields, termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields sortFields: indexPattern.fields.filter( @@ -113,8 +133,14 @@ export class UpdateSourceEditor extends Component { this.props.onChange({ propName: 'tooltipProperties', value: propertyNames }); }; - onUseTopHitsChange = event => { - this.props.onChange({ propName: 'useTopHits', value: event.target.checked }); + _onScalingTypeChange = optionId => { + const layerType = + optionId === SCALING_TYPES.CLUSTERS ? LAYER_TYPE.BLENDED_VECTOR : LAYER_TYPE.VECTOR; + this.props.onChange({ propName: 'scalingType', value: optionId, newLayerType: layerType }); + }; + + _onFilterByMapBoundsChange = event => { + this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked }); }; onTopHitsSplitFieldChange = topHitsSplitField => { @@ -133,29 +159,7 @@ export class UpdateSourceEditor extends Component { this.props.onChange({ propName: 'topHitsSize', value: size }); }; - renderTopHitsForm() { - const topHitsSwitch = ( - - - - ); - - if (!this.props.useTopHits) { - return topHitsSwitch; - } - + _renderTopHitsForm() { let sizeSlider; if (this.props.topHitsSplitField) { sizeSlider = ( @@ -183,7 +187,6 @@ export class UpdateSourceEditor extends Component { return ( - {topHitsSwitch} +
+ ); + } + + _renderScalingPanel() { + const scalingOptions = [ + { + id: SCALING_TYPES.LIMIT, + label: i18n.translate('xpack.maps.source.esSearch.limitScalingLabel', { + defaultMessage: 'Limit results to {maxResultWindow}.', + values: { maxResultWindow: this.state.maxResultWindow }, + }), + }, + { + id: SCALING_TYPES.TOP_HITS, + label: i18n.translate('xpack.maps.source.esSearch.useTopHitsLabel', { + defaultMessage: 'Show top hits per entity.', + }), + }, + ]; + if (this.state.supportsClustering) { + scalingOptions.push({ + id: SCALING_TYPES.CLUSTERS, + label: i18n.translate('xpack.maps.source.esSearch.clusterScalingLabel', { + defaultMessage: 'Show clusters when results exceed {maxResultWindow}.', + values: { maxResultWindow: this.state.maxResultWindow }, + }), + }); + } - - {this.renderTopHitsForm()} + let filterByBoundsSwitch; + if (this.props.scalingType !== SCALING_TYPES.CLUSTERS) { + filterByBoundsSwitch = ( + + + + ); + } + + let scalingForm = null; + if (this.props.scalingType === SCALING_TYPES.TOP_HITS) { + scalingForm = ( + + + {this._renderTopHitsForm()} + + ); + } + + return ( + + +
+ +
+
+ + + + + + + + {filterByBoundsSwitch} + + {scalingForm}
); } @@ -302,6 +379,9 @@ export class UpdateSourceEditor extends Component { {this._renderSortPanel()} + + {this._renderScalingPanel()} +
); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js index badfba7665dfd..e8a845c4b1669 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js @@ -16,6 +16,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { UpdateSourceEditor } from './update_source_editor'; +import { SCALING_TYPES } from '../../../../common/constants'; const defaultProps = { indexPatternId: 'indexPattern1', @@ -23,7 +24,7 @@ const defaultProps = { filterByMapBounds: true, tooltipFields: [], sortOrder: 'DESC', - useTopHits: false, + scalingType: SCALING_TYPES.LIMIT, topHitsSplitField: 'trackId', topHitsSize: 1, }; @@ -40,8 +41,10 @@ test('should enable sort order select when sort field provided', async () => { expect(component).toMatchSnapshot(); }); -test('should render top hits form when useTopHits is true', async () => { - const component = shallow(); +test('should render top hits form when scaling type is TOP_HITS', async () => { + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.d.ts index 25c4fae89f024..963a30c7413e8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.d.ts @@ -6,12 +6,31 @@ import { AbstractVectorSource } from './vector_source'; import { IVectorSource } from './vector_source'; -import { IndexPattern } from '../../../../../../../src/plugins/data/public'; +import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/public'; +import { VectorLayerRequestMeta } from '../../../common/data_request_descriptor_types'; export interface IESSource extends IVectorSource { + getId(): string; getIndexPattern(): Promise; + getIndexPatternId(): string; + getGeoFieldName(): string; + getMaxResultWindow(): Promise; + makeSearchSource( + searchFilters: VectorLayerRequestMeta, + limit: number, + initialSearchContext?: object + ): Promise; } export class AbstractESSource extends AbstractVectorSource implements IESSource { + getId(): string; getIndexPattern(): Promise; + getIndexPatternId(): string; + getGeoFieldName(): string; + getMaxResultWindow(): Promise; + makeSearchSource( + searchFilters: VectorLayerRequestMeta, + limit: number, + initialSearchContext?: object + ): Promise; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 1552db277e609..c5bf9a8be75bd 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -35,6 +35,10 @@ export class AbstractESSource extends AbstractVectorSource { ); } + getId() { + return this._descriptor.id; + } + isFieldAware() { return true; } @@ -48,12 +52,12 @@ export class AbstractESSource extends AbstractVectorSource { } getIndexPatternIds() { - return [this._descriptor.indexPatternId]; + return [this.getIndexPatternId()]; } getQueryableIndexPatternIds() { if (this.getApplyGlobalQuery()) { - return [this._descriptor.indexPatternId]; + return [this.getIndexPatternId()]; } return []; } @@ -106,7 +110,7 @@ export class AbstractESSource extends AbstractVectorSource { } } - async _makeSearchSource(searchFilters, limit, initialSearchContext) { + async makeSearchSource(searchFilters, limit, initialSearchContext) { const indexPattern = await this.getIndexPattern(); const isTimeAware = await this.isTimeAware(); const applyGlobalQuery = _.get(searchFilters, 'applyGlobalQuery', true); @@ -143,7 +147,7 @@ export class AbstractESSource extends AbstractVectorSource { } async getBoundsForFilters({ sourceQuery, query, timeFilters, filters, applyGlobalQuery }) { - const searchSource = await this._makeSearchSource( + const searchSource = await this.makeSearchSource( { sourceQuery, query, timeFilters, filters, applyGlobalQuery }, 0 ); @@ -190,19 +194,27 @@ export class AbstractESSource extends AbstractVectorSource { } } + getIndexPatternId() { + return this._descriptor.indexPatternId; + } + + getGeoFieldName() { + return this._descriptor.geoField; + } + async getIndexPattern() { if (this.indexPattern) { return this.indexPattern; } try { - this.indexPattern = await indexPatternService.get(this._descriptor.indexPatternId); + this.indexPattern = await indexPatternService.get(this.getIndexPatternId()); return this.indexPattern; } catch (error) { throw new Error( i18n.translate('xpack.maps.source.esSource.noIndexPatternErrorMessage', { defaultMessage: `Unable to find Index pattern for id: {indexPatternId}`, - values: { indexPatternId: this._descriptor.indexPatternId }, + values: { indexPatternId: this.getIndexPatternId() }, }) ); } @@ -219,7 +231,7 @@ export class AbstractESSource extends AbstractVectorSource { } } - async _getGeoField() { + _getGeoField = async () => { const indexPattern = await this.getIndexPattern(); const geoField = indexPattern.fields.getByName(this._descriptor.geoField); if (!geoField) { @@ -231,7 +243,7 @@ export class AbstractESSource extends AbstractVectorSource { ); } return geoField; - } + }; async getDisplayName() { try { @@ -239,7 +251,7 @@ export class AbstractESSource extends AbstractVectorSource { return indexPattern.title; } catch (error) { // Unable to load index pattern, just return id as display name - return this._descriptor.indexPatternId; + return this.getIndexPatternId(); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js index c12b4befc0684..3ce0fb58aba19 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js @@ -51,10 +51,6 @@ export class ESTermSource extends AbstractESAggSource { return _.has(this._descriptor, 'indexPatternId') && _.has(this._descriptor, 'term'); } - getIndexPatternIds() { - return [this._descriptor.indexPatternId]; - } - getTermField() { return this._termField; } @@ -90,7 +86,7 @@ export class ESTermSource extends AbstractESAggSource { } const indexPattern = await this.getIndexPattern(); - const searchSource = await this._makeSearchSource(searchFilters, 0); + const searchSource = await this.makeSearchSource(searchFilters, 0); const termsField = getField(indexPattern, this._termField.getName()); const termsAgg = { size: DEFAULT_MAX_BUCKETS_LIMIT }; searchSource.setField('aggs', { @@ -126,7 +122,7 @@ export class ESTermSource extends AbstractESAggSource { async getDisplayName() { //no need to localize. this is never rendered. - return `es_table ${this._descriptor.indexPatternId}`; + return `es_table ${this.getIndexPatternId()}`; } async filterAndFormatPropertiesToHtml(properties) { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/source.d.ts index b5b34efabda0a..2ca18e47a4bf9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.d.ts @@ -10,10 +10,14 @@ import { ILayer } from '../layer'; export interface ISource { createDefaultLayer(): ILayer; getDisplayName(): Promise; + destroy(): void; + getInspectorAdapters(): object; } export class AbstractSource implements ISource { constructor(sourceDescriptor: AbstractSourceDescriptor, inspectorAdapters: object); createDefaultLayer(): ILayer; getDisplayName(): Promise; + destroy(): void; + getInspectorAdapters(): object; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts index 7de3fe1823cb7..14fc23751ac1a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts @@ -7,13 +7,9 @@ import { AbstractSource, ISource } from './source'; import { IField } from '../fields/field'; +import { ESSearchSourceResponseMeta } from '../../../common/data_request_descriptor_types'; -export type GeoJsonFetchMeta = { - areResultsTrimmed: boolean; - areEntitiesTrimmed?: boolean; - entityCount?: number; - totalEntities?: number; -}; +export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; export type GeoJsonWithMeta = { data: unknown; // geojson feature collection diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js index 0f74dd605c8f1..7ff1c735c8613 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js @@ -98,10 +98,6 @@ export class AbstractVectorSource extends AbstractSource { return false; } - isFilterByMapBoundsConfigurable() { - return false; - } - isBoundsAware() { return false; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 8e05cf287efa6..acc26e5fce699 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -69,7 +69,7 @@ export class VectorStyleEditor extends Component { }; //These are all fields (only used for text labeling) - const fields = await this.props.layer.getFields(); + const fields = await this.props.layer.getStyleEditorFields(); const fieldPromises = fields.map(getFieldMeta); const fieldsArrayAll = await Promise.all(fieldPromises); if (!this._isMounted || _.isEqual(fieldsArrayAll, this.state.fields)) { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js index f74deb17fff7c..5b5028f68f08c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js @@ -71,7 +71,7 @@ class MockLayer { return new MockStyle(); } - findDataRequestById() { + getDataRequest() { return null; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts index f4c487b28757e..25063944b8891 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts @@ -7,13 +7,17 @@ import { IStyleProperty } from './style_property'; import { FIELD_ORIGIN } from '../../../../../common/constants'; -import { FieldMetaOptions } from '../../../../../common/style_property_descriptor_types'; +import { + FieldMetaOptions, + DynamicStylePropertyOptions, +} from '../../../../../common/style_property_descriptor_types'; import { IField } from '../../../fields/field'; import { IVectorLayer } from '../../../vector_layer'; import { IVectorSource } from '../../../sources/vector_source'; import { CategoryFieldMeta, RangeFieldMeta } from '../../../../../common/descriptor_types'; export interface IDynamicStyleProperty extends IStyleProperty { + getOptions(): DynamicStylePropertyOptions; getFieldMetaOptions(): FieldMetaOptions; getField(): IField | undefined; getFieldName(): string; @@ -22,6 +26,7 @@ export interface IDynamicStyleProperty extends IStyleProperty { getRangeFieldMeta(): RangeFieldMeta; getCategoryFieldMeta(): CategoryFieldMeta; isFieldMetaEnabled(): boolean; + isOrdinal(): boolean; supportsFieldMeta(): boolean; getFieldMetaRequest(): Promise; supportsMbFeatureState(): boolean; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index 030d3a2a1ef87..68e06bacfa7b7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -62,7 +62,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return rangeFieldMetaFromLocalFeatures; } - const styleMetaDataRequest = this._layer.findDataRequestById(dataRequestId); + const styleMetaDataRequest = this._layer.getDataRequest(dataRequestId); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { return rangeFieldMetaFromLocalFeatures; } @@ -87,7 +87,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return categoryFieldMetaFromLocalFeatures; } - const styleMetaDataRequest = this._layer.findDataRequestById(dataRequestId); + const styleMetaDataRequest = this._layer.getDataRequest(dataRequestId); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { return categoryFieldMetaFromLocalFeatures; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts index bba6cdb48e672..6c00c01dec442 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts @@ -34,8 +34,8 @@ export interface IStyleProperty { } export class AbstractStyleProperty implements IStyleProperty { - private _options: StylePropertyOptions; - private _styleName: string; + private readonly _options: StylePropertyOptions; + private readonly _styleName: string; constructor(options: StylePropertyOptions, styleName: string) { this._options = options; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.d.ts b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.d.ts new file mode 100644 index 0000000000000..ac84a3b6447d2 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.d.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IStyleProperty } from './properties/style_property'; +import { IDynamicStyleProperty } from './properties/dynamic_style_property'; +import { IVectorLayer } from '../../vector_layer'; +import { IVectorSource } from '../../sources/vector_source'; + +export interface IVectorStyle { + getAllStyleProperties(): IStyleProperty[]; + getDescriptor(): object; + getDynamicPropertiesArray(): IDynamicStyleProperty[]; +} + +export class VectorStyle implements IVectorStyle { + constructor(descriptor: unknown, source: IVectorSource, layer: IVectorLayer); + + getAllStyleProperties(): IStyleProperty[]; + getDescriptor(): object; + getDynamicPropertiesArray(): IDynamicStyleProperty[]; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 1c8ff3e205a38..6ad60e15f10e1 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -123,7 +123,7 @@ export class VectorStyle extends AbstractStyle { ); } - _getAllStyleProperties() { + getAllStyleProperties() { return [ this._symbolizeAsStyleProperty, this._iconStyleProperty, @@ -164,7 +164,7 @@ export class VectorStyle extends AbstractStyle { }); const styleProperties = {}; - this._getAllStyleProperties().forEach(styleProperty => { + this.getAllStyleProperties().forEach(styleProperty => { styleProperties[styleProperty.getStyleName()] = styleProperty; }); @@ -339,7 +339,7 @@ export class VectorStyle extends AbstractStyle { } getDynamicPropertiesArray() { - const styleProperties = this._getAllStyleProperties(); + const styleProperties = this.getAllStyleProperties(); return styleProperties.filter( styleProperty => styleProperty.isDynamic() && styleProperty.isComplete() ); @@ -390,7 +390,7 @@ export class VectorStyle extends AbstractStyle { return null; } - const formattersDataRequest = this._layer.findDataRequestById(dataRequestId); + const formattersDataRequest = this._layer.getDataRequest(dataRequestId); if (!formattersDataRequest || !formattersDataRequest.hasData()) { return null; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js index 8bc397dd98b56..dd2cf79318d8e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js @@ -16,7 +16,7 @@ import chrome from 'ui/chrome'; export const MIN_SIZE = 1; export const MAX_SIZE = 64; -export const DEFAULT_MIN_SIZE = 4; +export const DEFAULT_MIN_SIZE = 7; // Make default large enough to fit default label size export const DEFAULT_MAX_SIZE = 32; export const DEFAULT_SIGMA = 3; export const DEFAULT_LABEL_SIZE = 14; diff --git a/x-pack/legacy/plugins/maps/public/layers/tile_layer.js b/x-pack/legacy/plugins/maps/public/layers/tile_layer.js index b35adcad976c3..aa2619e96f834 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tile_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/tile_layer.js @@ -30,7 +30,7 @@ export class TileLayer extends AbstractLayer { const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, dataFilters); try { - const url = await this._source.getUrlTemplate(); + const url = await this.getSource().getUrlTemplate(); stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, url, {}); } catch (error) { onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); diff --git a/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts b/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts index 065fbd79d9789..8ce38a128ebc4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts +++ b/x-pack/legacy/plugins/maps/public/layers/tile_layer.test.ts @@ -17,7 +17,7 @@ const sourceDescriptor: XYZTMSSourceDescriptor = { }; class MockTileSource implements ITMSSource { - private _descriptor: XYZTMSSourceDescriptor; + private readonly _descriptor: XYZTMSSourceDescriptor; constructor(descriptor: XYZTMSSourceDescriptor) { this._descriptor = descriptor; } @@ -32,6 +32,14 @@ class MockTileSource implements ITMSSource { async getUrlTemplate(): Promise { return 'template/{x}/{y}/{z}.png'; } + + destroy(): void { + // no-op + } + + getInspectorAdapters(): object { + return {}; + } } describe('TileLayer', () => { diff --git a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js b/x-pack/legacy/plugins/maps/public/layers/util/data_request.ts similarity index 61% rename from x-pack/legacy/plugins/maps/public/layers/util/data_request.js rename to x-pack/legacy/plugins/maps/public/layers/util/data_request.ts index 3a6c10a9f07a6..e361574194628 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/data_request.ts @@ -3,42 +3,47 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable max-classes-per-file */ + import _ from 'lodash'; +import { DataRequestDescriptor, DataMeta } from '../../../common/data_request_descriptor_types'; export class DataRequest { - constructor(descriptor) { + private readonly _descriptor: DataRequestDescriptor; + + constructor(descriptor: DataRequestDescriptor) { this._descriptor = { ...descriptor, }; } - getData() { + getData(): object | undefined { return this._descriptor.data; } - isLoading() { + isLoading(): boolean { return !!this._descriptor.dataRequestToken; } - getMeta() { + getMeta(): DataMeta { return this.hasData() ? _.get(this._descriptor, 'dataMeta', {}) : _.get(this._descriptor, 'dataMetaAtStart', {}); } - hasData() { + hasData(): boolean { return !!this._descriptor.data; } - hasDataOrRequestInProgress() { - return this._descriptor.data || this._descriptor.dataRequestToken; + hasDataOrRequestInProgress(): boolean { + return this.hasData() || this.isLoading(); } - getDataId() { + getDataId(): string { return this._descriptor.dataId; } - getRequestToken() { + getRequestToken(): symbol | undefined { return this._descriptor.dataRequestToken; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts b/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts index 748b2fd1d782c..77e8ab768cd00 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts @@ -8,20 +8,43 @@ import { AbstractLayer } from './layer'; import { IVectorSource } from './sources/vector_source'; import { VectorLayerDescriptor } from '../../common/descriptor_types'; +import { MapFilters, VectorLayerRequestMeta } from '../../common/data_request_descriptor_types'; import { ILayer } from './layer'; import { IJoin } from './joins/join'; +import { IVectorStyle } from './styles/vector/vector_style'; +import { IField } from './fields/field'; +import { SyncContext } from '../actions/map_actions'; type VectorLayerArguments = { source: IVectorSource; + joins: IJoin[]; layerDescriptor: VectorLayerDescriptor; }; export interface IVectorLayer extends ILayer { + getFields(): Promise; + getStyleEditorFields(): Promise; getValidJoins(): IJoin[]; } export class VectorLayer extends AbstractLayer implements IVectorLayer { + static createDescriptor( + options: VectorLayerArguments, + mapColors: string[] + ): VectorLayerDescriptor; + + protected readonly _source: IVectorSource; + protected readonly _style: IVectorStyle; + constructor(options: VectorLayerArguments); + getFields(): Promise; + getStyleEditorFields(): Promise; getValidJoins(): IJoin[]; + _getSearchFilters( + dataFilters: MapFilters, + source: IVectorSource, + style: IVectorStyle + ): VectorLayerRequestMeta; + _syncData(syncContext: SyncContext, source: IVectorSource, style: IVectorStyle): Promise; } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 70bba3d91c723..6b89554546330 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -58,7 +58,7 @@ export class VectorLayer extends AbstractLayer { constructor({ layerDescriptor, source, joins = [] }) { super({ layerDescriptor, source }); this._joins = joins; - this._style = new VectorStyle(this._descriptor.style, this._source, this); + this._style = new VectorStyle(this._descriptor.style, source, this); } getStyle() { @@ -66,10 +66,10 @@ export class VectorLayer extends AbstractLayer { } destroy() { - if (this._source) { - this._source.destroy(); + if (this.getSource()) { + this.getSource().destroy(); } - this._joins.forEach(joinSource => { + this.getJoins().forEach(joinSource => { joinSource.destroy(); }); } @@ -79,7 +79,7 @@ export class VectorLayer extends AbstractLayer { } getValidJoins() { - return this._joins.filter(join => { + return this.getJoins().filter(join => { return join.hasCompleteConfig(); }); } @@ -119,7 +119,7 @@ export class VectorLayer extends AbstractLayer { } if ( - this._joins.length && + this.getJoins().length && !featureCollection.features.some(feature => feature.properties[FEATURE_VISIBLE_PROPERTY_NAME]) ) { return { @@ -131,11 +131,11 @@ export class VectorLayer extends AbstractLayer { } const sourceDataRequest = this.getSourceDataRequest(); - const { tooltipContent, areResultsTrimmed } = this._source.getSourceTooltipContent( + const { tooltipContent, areResultsTrimmed } = this.getSource().getSourceTooltipContent( sourceDataRequest ); return { - icon: this._style.getIcon(), + icon: this.getCurrentStyle().getIcon(), tooltipContent: tooltipContent, areResultsTrimmed: areResultsTrimmed, }; @@ -146,11 +146,11 @@ export class VectorLayer extends AbstractLayer { } async hasLegendDetails() { - return this._style.hasLegendDetails(); + return this.getCurrentStyle().hasLegendDetails(); } renderLegendDetails() { - return this._style.renderLegendDetails(); + return this.getCurrentStyle().renderLegendDetails(); } _getBoundsBasedOnData() { @@ -175,17 +175,22 @@ export class VectorLayer extends AbstractLayer { } async getBounds(dataFilters) { - const isStaticLayer = !this._source.isBoundsAware() || !this._source.isFilterByMapBounds(); + const isStaticLayer = + !this.getSource().isBoundsAware() || !this.getSource().isFilterByMapBounds(); if (isStaticLayer) { return this._getBoundsBasedOnData(); } - const searchFilters = this._getSearchFilters(dataFilters); - return await this._source.getBoundsForFilters(searchFilters); + const searchFilters = this._getSearchFilters( + dataFilters, + this.getSource(), + this.getCurrentStyle() + ); + return await this.getSource().getBoundsForFilters(searchFilters); } async getLeftJoinFields() { - return await this._source.getLeftJoinFields(); + return await this.getSource().getLeftJoinFields(); } _getJoinFields() { @@ -198,12 +203,17 @@ export class VectorLayer extends AbstractLayer { } async getFields() { - const sourceFields = await this._source.getFields(); + const sourceFields = await this.getSource().getFields(); + return [...sourceFields, ...this._getJoinFields()]; + } + + async getStyleEditorFields() { + const sourceFields = await this.getSourceForEditing().getFields(); return [...sourceFields, ...this._getJoinFields()]; } getIndexPatternIds() { - const indexPatternIds = this._source.getIndexPatternIds(); + const indexPatternIds = this.getSource().getIndexPatternIds(); this.getValidJoins().forEach(join => { indexPatternIds.push(...join.getIndexPatternIds()); }); @@ -211,17 +221,13 @@ export class VectorLayer extends AbstractLayer { } getQueryableIndexPatternIds() { - const indexPatternIds = this._source.getQueryableIndexPatternIds(); + const indexPatternIds = this.getSource().getQueryableIndexPatternIds(); this.getValidJoins().forEach(join => { indexPatternIds.push(...join.getQueryableIndexPatternIds()); }); return indexPatternIds; } - findDataRequestById(sourceDataId) { - return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); - } - async _syncJoin({ join, startLoading, @@ -239,7 +245,7 @@ export class VectorLayer extends AbstractLayer { sourceQuery: joinSource.getWhereQuery(), applyGlobalQuery: joinSource.getApplyGlobalQuery(), }; - const prevDataRequest = this.findDataRequestById(sourceDataId); + const prevDataRequest = this.getDataRequest(sourceDataId); const canSkipFetch = await canSkipSourceUpdate({ source: joinSource, @@ -281,30 +287,30 @@ export class VectorLayer extends AbstractLayer { } } - async _syncJoins(syncContext) { + async _syncJoins(syncContext, style) { const joinSyncs = this.getValidJoins().map(async join => { - await this._syncJoinStyleMeta(syncContext, join); - await this._syncJoinFormatters(syncContext, join); + await this._syncJoinStyleMeta(syncContext, join, style); + await this._syncJoinFormatters(syncContext, join, style); return this._syncJoin({ join, ...syncContext }); }); return await Promise.all(joinSyncs); } - _getSearchFilters(dataFilters) { + _getSearchFilters(dataFilters, source, style) { const fieldNames = [ - ...this._source.getFieldNames(), - ...this._style.getSourceFieldNames(), + ...source.getFieldNames(), + ...style.getSourceFieldNames(), ...this.getValidJoins().map(join => join.getLeftField().getName()), ]; return { ...dataFilters, fieldNames: _.uniq(fieldNames).sort(), - geogridPrecision: this._source.getGeoGridPrecision(dataFilters.zoom), + geogridPrecision: source.getGeoGridPrecision(dataFilters.zoom), sourceQuery: this.getQuery(), - applyGlobalQuery: this._source.getApplyGlobalQuery(), - sourceMeta: this._source.getSyncMeta(), + applyGlobalQuery: source.getApplyGlobalQuery(), + sourceMeta: source.getSyncMeta(), }; } @@ -347,20 +353,21 @@ export class VectorLayer extends AbstractLayer { } } - async _syncSource({ - startLoading, - stopLoading, - onLoadError, - registerCancelCallback, - dataFilters, - isRequestStillActive, - }) { + async _syncSource(syncContext, source, style) { + const { + startLoading, + stopLoading, + onLoadError, + registerCancelCallback, + dataFilters, + isRequestStillActive, + } = syncContext; const dataRequestId = SOURCE_DATA_ID_ORIGIN; const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); - const searchFilters = this._getSearchFilters(dataFilters); + const searchFilters = this._getSearchFilters(dataFilters, source, style); const prevDataRequest = this.getSourceDataRequest(); const canSkipFetch = await canSkipSourceUpdate({ - source: this._source, + source, prevDataRequest, nextMeta: searchFilters, }); @@ -373,8 +380,8 @@ export class VectorLayer extends AbstractLayer { try { startLoading(dataRequestId, requestToken, searchFilters); - const layerName = await this.getDisplayName(); - const { data: sourceFeatureCollection, meta } = await this._source.getGeoJsonWithMeta( + const layerName = await this.getDisplayName(source); + const { data: sourceFeatureCollection, meta } = await source.getGeoJsonWithMeta( layerName, searchFilters, registerCancelCallback.bind(null, requestToken), @@ -398,16 +405,17 @@ export class VectorLayer extends AbstractLayer { } } - async _syncSourceStyleMeta(syncContext) { - if (this._style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { + async _syncSourceStyleMeta(syncContext, source, style) { + if (this.getCurrentStyle().constructor.type !== LAYER_STYLE_TYPE.VECTOR) { return; } return this._syncStyleMeta({ - source: this._source, + source, + style, sourceQuery: this.getQuery(), dataRequestId: SOURCE_META_ID_ORIGIN, - dynamicStyleProps: this._style.getDynamicPropertiesArray().filter(dynamicStyleProp => { + dynamicStyleProps: style.getDynamicPropertiesArray().filter(dynamicStyleProp => { return ( dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE && dynamicStyleProp.isFieldMetaEnabled() @@ -417,28 +425,32 @@ export class VectorLayer extends AbstractLayer { }); } - async _syncJoinStyleMeta(syncContext, join) { + async _syncJoinStyleMeta(syncContext, join, style) { const joinSource = join.getRightJoinSource(); return this._syncStyleMeta({ source: joinSource, + style, sourceQuery: joinSource.getWhereQuery(), dataRequestId: join.getSourceMetaDataRequestId(), - dynamicStyleProps: this._style.getDynamicPropertiesArray().filter(dynamicStyleProp => { - const matchingField = joinSource.getMetricFieldForName( - dynamicStyleProp.getField().getName() - ); - return ( - dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && - !!matchingField && - dynamicStyleProp.isFieldMetaEnabled() - ); - }), + dynamicStyleProps: this.getCurrentStyle() + .getDynamicPropertiesArray() + .filter(dynamicStyleProp => { + const matchingField = joinSource.getMetricFieldForName( + dynamicStyleProp.getField().getName() + ); + return ( + dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && + !!matchingField && + dynamicStyleProp.isFieldMetaEnabled() + ); + }), ...syncContext, }); } async _syncStyleMeta({ source, + style, sourceQuery, dataRequestId, dynamicStyleProps, @@ -459,10 +471,10 @@ export class VectorLayer extends AbstractLayer { const nextMeta = { dynamicStyleFields: _.uniq(dynamicStyleFields).sort(), sourceQuery, - isTimeAware: this._style.isTimeAware() && (await source.isTimeAware()), + isTimeAware: this.getCurrentStyle().isTimeAware() && (await source.isTimeAware()), timeFilters: dataFilters.timeFilters, }; - const prevDataRequest = this.findDataRequestById(dataRequestId); + const prevDataRequest = this.getDataRequest(dataRequestId); const canSkipFetch = canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }); if (canSkipFetch) { return; @@ -471,10 +483,10 @@ export class VectorLayer extends AbstractLayer { const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); try { startLoading(dataRequestId, requestToken, nextMeta); - const layerName = await this.getDisplayName(); + const layerName = await this.getDisplayName(source); const styleMeta = await source.loadStylePropsMeta( layerName, - this._style, + style, dynamicStyleProps, registerCancelCallback, nextMeta @@ -487,15 +499,15 @@ export class VectorLayer extends AbstractLayer { } } - async _syncSourceFormatters(syncContext) { - if (this._style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { + async _syncSourceFormatters(syncContext, source, style) { + if (style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { return; } return this._syncFormatters({ - source: this._source, + source, dataRequestId: SOURCE_FORMATTERS_ID_ORIGIN, - fields: this._style + fields: style .getDynamicPropertiesArray() .filter(dynamicStyleProp => { return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE; @@ -507,12 +519,12 @@ export class VectorLayer extends AbstractLayer { }); } - async _syncJoinFormatters(syncContext, join) { + async _syncJoinFormatters(syncContext, join, style) { const joinSource = join.getRightJoinSource(); return this._syncFormatters({ source: joinSource, dataRequestId: join.getSourceFormattersDataRequestId(), - fields: this._style + fields: style .getDynamicPropertiesArray() .filter(dynamicStyleProp => { const matchingField = joinSource.getMetricFieldForName( @@ -538,7 +550,7 @@ export class VectorLayer extends AbstractLayer { const nextMeta = { fieldNames: _.uniq(fieldNames).sort(), }; - const prevDataRequest = this.findDataRequestById(dataRequestId); + const prevDataRequest = this.getDataRequest(dataRequestId); const canSkipUpdate = canSkipFormattersUpdate({ prevDataRequest, nextMeta }); if (canSkipUpdate) { return; @@ -565,13 +577,27 @@ export class VectorLayer extends AbstractLayer { } async syncData(syncContext) { + this._syncData(syncContext, this.getSource(), this.getCurrentStyle()); + } + + // TLDR: Do not call getSource or getCurrentStyle in syncData flow. Use 'source' and 'style' arguments instead. + // + // 1) State is contained in the redux store. Layer instance state is readonly. + // 2) Even though data request descriptor updates trigger new instances for rendering, + // syncing data executes on a single object instance. Syncing data can not use updated redux store state. + // + // Blended layer data syncing branches on the source/style depending on whether clustering is used or not. + // Given 1 above, which source/style to use can not be stored in Layer instance state. + // Given 2 above, which source/style to use can not be pulled from data request state. + // Therefore, source and style are provided as arugments and must be used instead of calling getSource or getCurrentStyle. + async _syncData(syncContext, source, style) { if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; } - await this._syncSourceStyleMeta(syncContext); - await this._syncSourceFormatters(syncContext); - const sourceResult = await this._syncSource(syncContext); + await this._syncSourceStyleMeta(syncContext, source, style); + await this._syncSourceFormatters(syncContext, source, style); + const sourceResult = await this._syncSource(syncContext, source, style); if ( !sourceResult.featureCollection || !sourceResult.featureCollection.features.length || @@ -580,7 +606,7 @@ export class VectorLayer extends AbstractLayer { return; } - const joinStates = await this._syncJoins(syncContext); + const joinStates = await this._syncJoins(syncContext, style); await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); } @@ -596,7 +622,7 @@ export class VectorLayer extends AbstractLayer { if (!featureCollection) { if (featureCollectionOnMap) { - this._style.clearFeatureState(featureCollectionOnMap, mbMap, this.getId()); + this.getCurrentStyle().clearFeatureState(featureCollectionOnMap, mbMap, this.getId()); } mbGeoJSONSource.setData(EMPTY_FEATURE_COLLECTION); return; @@ -605,7 +631,7 @@ export class VectorLayer extends AbstractLayer { // "feature-state" data expressions are not supported with layout properties. // To work around this limitation, // scaled layout properties (like icon-size) must fall back to geojson property values :( - const hasGeoJsonProperties = this._style.setFeatureStateAndStyleProps( + const hasGeoJsonProperties = this.getCurrentStyle().setFeatureStateAndStyleProps( featureCollection, mbMap, this.getId() @@ -626,7 +652,7 @@ export class VectorLayer extends AbstractLayer { // Point layers symbolized as icons only contain a single mapbox layer. let markerLayerId; let textLayerId; - if (this._style.arePointsSymbolizedAsCircles()) { + if (this.getCurrentStyle().arePointsSymbolizedAsCircles()) { markerLayerId = pointLayerId; textLayerId = this._getMbTextLayerId(); if (symbolLayer) { @@ -680,13 +706,13 @@ export class VectorLayer extends AbstractLayer { mbMap.setFilter(textLayerId, filterExpr); } - this._style.setMBPaintPropertiesForPoints({ + this.getCurrentStyle().setMBPaintPropertiesForPoints({ alpha: this.getAlpha(), mbMap, pointLayerId, }); - this._style.setMBPropertiesForLabelText({ + this.getCurrentStyle().setMBPropertiesForLabelText({ alpha: this.getAlpha(), mbMap, textLayerId, @@ -711,13 +737,13 @@ export class VectorLayer extends AbstractLayer { mbMap.setFilter(symbolLayerId, filterExpr); } - this._style.setMBSymbolPropertiesForPoints({ + this.getCurrentStyle().setMBSymbolPropertiesForPoints({ alpha: this.getAlpha(), mbMap, symbolLayerId, }); - this._style.setMBPropertiesForLabelText({ + this.getCurrentStyle().setMBPropertiesForLabelText({ alpha: this.getAlpha(), mbMap, textLayerId: symbolLayerId, @@ -745,7 +771,7 @@ export class VectorLayer extends AbstractLayer { paint: {}, }); } - this._style.setMBPaintProperties({ + this.getCurrentStyle().setMBPaintProperties({ alpha: this.getAlpha(), mbMap, fillLayerId, @@ -830,9 +856,13 @@ export class VectorLayer extends AbstractLayer { for (let i = 0; i < tooltipsFromSource.length; i++) { const tooltipProperty = tooltipsFromSource[i]; const matchingJoins = []; - for (let j = 0; j < this._joins.length; j++) { - if (this._joins[j].getLeftField().getName() === tooltipProperty.getPropertyKey()) { - matchingJoins.push(this._joins[j]); + for (let j = 0; j < this.getJoins().length; j++) { + if ( + this.getJoins() + [j].getLeftField() + .getName() === tooltipProperty.getPropertyKey() + ) { + matchingJoins.push(this.getJoins()[j]); } } if (matchingJoins.length) { @@ -842,18 +872,22 @@ export class VectorLayer extends AbstractLayer { } async getPropertiesForTooltip(properties) { - let allTooltips = await this._source.filterAndFormatPropertiesToHtml(properties); + let allTooltips = await this.getSource().filterAndFormatPropertiesToHtml(properties); this._addJoinsToSourceTooltips(allTooltips); - for (let i = 0; i < this._joins.length; i++) { - const propsFromJoin = await this._joins[i].filterAndFormatPropertiesForTooltip(properties); + for (let i = 0; i < this.getJoins().length; i++) { + const propsFromJoin = await this.getJoins()[i].filterAndFormatPropertiesForTooltip( + properties + ); allTooltips = [...allTooltips, ...propsFromJoin]; } return allTooltips; } canShowTooltip() { - return this.isVisible() && (this._source.canFormatFeatureProperties() || this._joins.length); + return ( + this.isVisible() && (this.getSource().canFormatFeatureProperties() || this.getJoins().length) + ); } getFeatureById(id) { diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js index b09ccdc3af8ba..44987fd3e78f0 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js @@ -48,7 +48,7 @@ export class VectorTileLayer extends TileLayer { return; } - const nextMeta = { tileLayerId: this._source.getTileLayerId() }; + const nextMeta = { tileLayerId: this.getSource().getTileLayerId() }; const canSkipSync = this._canSkipSync({ prevDataRequest: this.getSourceDataRequest(), nextMeta, @@ -60,7 +60,7 @@ export class VectorTileLayer extends TileLayer { const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); try { startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, dataFilters); - const styleAndSprites = await this._source.getVectorStyleSheetAndSpriteMeta(isRetina()); + const styleAndSprites = await this.getSource().getVectorStyleSheetAndSpriteMeta(isRetina()); const spriteSheetImageData = await loadSpriteSheetImageData(styleAndSprites.spriteMeta.png); const data = { ...styleAndSprites, @@ -78,7 +78,7 @@ export class VectorTileLayer extends TileLayer { _generateMbSourceIdPrefix() { const DELIMITTER = '___'; - return `${this.getId()}${DELIMITTER}${this._source.getTileLayerId()}${DELIMITTER}`; + return `${this.getId()}${DELIMITTER}${this.getSource().getTileLayerId()}${DELIMITTER}`; } _generateMbSourceId(name) { @@ -141,7 +141,7 @@ export class VectorTileLayer extends TileLayer { } _makeNamespacedImageId(imageId) { - const prefix = this._source.getSpriteNamespacePrefix() + '/'; + const prefix = this.getSource().getSpriteNamespacePrefix() + '/'; return prefix + imageId; } diff --git a/x-pack/legacy/plugins/maps/public/meta.js b/x-pack/legacy/plugins/maps/public/meta.js index c5cfb582976c1..4d81785ff7a0a 100644 --- a/x-pack/legacy/plugins/maps/public/meta.js +++ b/x-pack/legacy/plugins/maps/public/meta.js @@ -9,6 +9,7 @@ import { EMS_FILES_CATALOGUE_PATH, EMS_TILES_CATALOGUE_PATH, EMS_GLYPHS_PATH, + EMS_APP_NAME, } from '../common/constants'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; @@ -56,7 +57,8 @@ export function getEMSClient() { emsClient = new EMSClient({ language: i18n.getLocale(), - kbnVersion: chrome.getInjected('kbnPkgVersion'), + appVersion: chrome.getInjected('kbnPkgVersion'), + appName: EMS_APP_NAME, tileApiUrl, fileApiUrl, landingPageUrl: chrome.getInjected('emsLandingPageUrl'), diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js index e5eaf8870aa77..79d890bc21f14 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js @@ -10,6 +10,7 @@ import { TileLayer } from '../layers/tile_layer'; import { VectorTileLayer } from '../layers/vector_tile_layer'; import { VectorLayer } from '../layers/vector_layer'; import { HeatmapLayer } from '../layers/heatmap_layer'; +import { BlendedVectorLayer } from '../layers/blended_vector_layer'; import { ALL_SOURCES } from '../layers/sources/all_sources'; import { timefilter } from 'ui/timefilter'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -40,6 +41,8 @@ function createLayerInstance(layerDescriptor, inspectorAdapters) { return new VectorTileLayer({ layerDescriptor, source }); case HeatmapLayer.type: return new HeatmapLayer({ layerDescriptor, source }); + case BlendedVectorLayer.type: + return new BlendedVectorLayer({ layerDescriptor, source }); default: throw new Error(`Unrecognized layerType ${layerDescriptor.type}`); } diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js index 5ec40a57ebc7f..ef2e23e51a092 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js @@ -5,6 +5,7 @@ */ jest.mock('../layers/vector_layer', () => {}); +jest.mock('../layers/blended_vector_layer', () => {}); jest.mock('../layers/heatmap_layer', () => {}); jest.mock('../layers/vector_tile_layer', () => {}); jest.mock('../layers/sources/all_sources', () => {}); diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index 757750dbb0813..7ca659148449f 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -5,6 +5,7 @@ */ import { + EMS_APP_NAME, EMS_CATALOGUE_PATH, EMS_FILES_API_PATH, EMS_FILES_CATALOGUE_PATH, @@ -38,7 +39,8 @@ export function initRoutes(server, licenseUid) { if (mapConfig.includeElasticMapsService) { emsClient = new EMSClient({ language: i18n.getLocale(), - kbnVersion: serverConfig.get('pkg.version'), + appVersion: serverConfig.get('pkg.version'), + appName: EMS_APP_NAME, fileApiUrl: mapConfig.emsFileApiUrl, tileApiUrl: mapConfig.emsTileApiUrl, landingPageUrl: mapConfig.emsLandingPageUrl, diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts index c14b64a32fb5c..b506784bf15ee 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts @@ -19,7 +19,6 @@ import { StateManagementConfigProvider, AppStateProvider, KbnUrlProvider, - RedirectWhenMissingProvider, npStart, } from '../legacy_imports'; @@ -79,8 +78,7 @@ function createLocalStateModule() { function createLocalKbnUrlModule() { angular .module('monitoring/KbnUrl', ['monitoring/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalConfigModule(core: AppMountContext['core']) { diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts index a2ebe8231456f..208b7e2acdb0f 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts @@ -18,5 +18,5 @@ export { AppStateProvider } from 'ui/state_management/app_state'; // @ts-ignore export { EventsProvider } from 'ui/events'; // @ts-ignore -export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; +export { KbnUrlProvider } from 'ui/url'; export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index 9ce4e807f8ef8..89e98302cddc9 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -10,7 +10,7 @@ import { resolve } from 'path'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; import { config as reportingConfig } from './config'; import { legacyInit } from './server/legacy'; -import { ReportingConfigOptions, ReportingPluginSpecOptions } from './types'; +import { ReportingPluginSpecOptions } from './types'; const kbToBase64Length = (kb: number) => { return Math.floor((kb * 1024 * 8) / 6); @@ -25,20 +25,6 @@ export const reporting = (kibana: any) => { config: reportingConfig, uiExports: { - shareContextMenuExtensions: [ - 'plugins/reporting/share_context_menu/register_csv_reporting', - 'plugins/reporting/share_context_menu/register_reporting', - ], - embeddableActions: ['plugins/reporting/panel_actions/get_csv_panel_action'], - home: ['plugins/reporting/register_feature'], - managementSections: ['plugins/reporting/views/management'], - injectDefaultVars(server: Legacy.Server, options?: ReportingConfigOptions) { - const config = server.config(); - return { - reportingPollConfig: options ? options.poll : {}, - enablePanelActionDownload: config.get('xpack.reporting.csv.enablePanelActionDownload'), - }; - }, uiSettingDefaults: { [UI_SETTINGS_CUSTOM_PDF_LOGO]: { name: i18n.translate('xpack.reporting.pdfFooterImageLabel', { diff --git a/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts b/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts deleted file mode 100644 index 9dd7cbb5fc567..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_info_button.test.mocks.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const mockJobQueueClient = { list: jest.fn(), total: jest.fn(), getInfo: jest.fn() }; -jest.mock('../lib/job_queue_client', () => ({ jobQueueClient: mockJobQueueClient })); diff --git a/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx deleted file mode 100644 index d78eb5c409c1f..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -interface JobData { - _index: string; - _id: string; - _source: { - browser_type: string; - created_at: string; - jobtype: string; - created_by: string; - payload: { - type: string; - title: string; - }; - kibana_name?: string; // undefined if job is pending (not yet claimed by an instance) - kibana_id?: string; // undefined if job is pending (not yet claimed by an instance) - output?: { content_type: string; size: number }; // undefined if job is incomplete - completed_at?: string; // undefined if job is incomplete - }; -} - -jest.mock('ui/chrome', () => ({ - getInjected() { - return { - jobsRefresh: { - interval: 10, - intervalErrorMultiplier: 2, - }, - }; - }, -})); - -jest.mock('ui/kfetch', () => ({ - kfetch: ({ pathname }: { pathname: string }): Promise => { - if (pathname === '/api/reporting/jobs/list') { - return Promise.resolve([ - { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore - ]); - } - - // query for jobs count - return Promise.resolve(18); - }, -})); - -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ReportListing } from './report_listing'; - -describe('ReportListing', () => { - it('Report job listing with some items', () => { - const wrapper = mountWithIntl( - - ); - wrapper.update(); - const input = wrapper.find('[data-test-subj="reportJobListing"]'); - expect(input).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/public/lib/download_report.ts b/x-pack/legacy/plugins/reporting/public/lib/download_report.ts deleted file mode 100644 index 54194c87afabc..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/download_report.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npStart } from 'ui/new_platform'; -import { API_BASE_URL } from '../../common/constants'; - -const { core } = npStart; - -export function getReportURL(jobId: string) { - const apiBaseUrl = core.http.basePath.prepend(API_BASE_URL); - const downloadLink = `${apiBaseUrl}/jobs/download/${jobId}`; - - return downloadLink; -} - -export function downloadReport(jobId: string) { - const location = getReportURL(jobId); - - window.open(location); -} diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts deleted file mode 100644 index 87d4174168b7f..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npStart } from 'ui/new_platform'; -import { API_LIST_URL } from '../../common/constants'; - -const { core } = npStart; - -export interface JobQueueEntry { - _id: string; - _source: any; -} - -export interface JobContent { - content: string; - content_type: boolean; -} - -export interface JobInfo { - kibana_name: string; - kibana_id: string; - browser_type: string; - created_at: string; - priority: number; - jobtype: string; - created_by: string; - timeout: number; - output: { - content_type: string; - size: number; - warnings: string[]; - }; - process_expiration: string; - completed_at: string; - payload: { - layout: { id: string; dimensions: { width: number; height: number } }; - objects: Array<{ relativeUrl: string }>; - type: string; - title: string; - forceNow: string; - browserTimezone: string; - }; - meta: { - layout: string; - objectType: string; - }; - max_attempts: number; - started_at: string; - attempts: number; - status: string; -} - -class JobQueueClient { - public list = (page = 0, jobIds: string[] = []): Promise => { - const query = { page } as any; - if (jobIds.length > 0) { - // Only getting the first 10, to prevent URL overflows - query.ids = jobIds.slice(0, 10).join(','); - } - - return core.http.get(`${API_LIST_URL}/list`, { - query, - asSystemRequest: true, - }); - }; - - public total(): Promise { - return core.http.get(`${API_LIST_URL}/count`, { - asSystemRequest: true, - }); - } - - public getContent(jobId: string): Promise { - return core.http.get(`${API_LIST_URL}/output/${jobId}`, { - asSystemRequest: true, - }); - } - - public getInfo(jobId: string): Promise { - return core.http.get(`${API_LIST_URL}/info/${jobId}`, { - asSystemRequest: true, - }); - } -} - -export const jobQueueClient = new JobQueueClient(); diff --git a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts b/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts deleted file mode 100644 index d471dc57fc9e1..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { stringify } from 'query-string'; -import { npStart } from 'ui/new_platform'; -// @ts-ignore -import rison from 'rison-node'; -import { add } from './job_completion_notifications'; - -const { core } = npStart; -const API_BASE_URL = '/api/reporting/generate'; - -interface JobParams { - [paramName: string]: any; -} - -export const getReportingJobPath = (exportType: string, jobParams: JobParams) => { - const params = stringify({ jobParams: rison.encode(jobParams) }); - - return `${core.http.basePath.prepend(API_BASE_URL)}/${exportType}?${params}`; -}; - -export const createReportingJob = async (exportType: string, jobParams: any) => { - const jobParamsRison = rison.encode(jobParams); - const resp = await core.http.post(`${API_BASE_URL}/${exportType}`, { - method: 'POST', - body: JSON.stringify({ - jobParams: jobParamsRison, - }), - }); - - add(resp.job.id); - - return resp; -}; diff --git a/x-pack/legacy/plugins/reporting/public/register_feature.ts b/x-pack/legacy/plugins/reporting/public/register_feature.ts deleted file mode 100644 index 4e8d32facfcec..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/register_feature.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; - -const { - plugins: { home }, -} = npSetup; - -home.featureCatalogue.register({ - id: 'reporting', - title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', { - defaultMessage: 'Reporting', - }), - description: i18n.translate('xpack.reporting.registerFeature.reportingDescription', { - defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', - }), - icon: 'reportingApp', - path: '/app/kibana#/management/kibana/reporting', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, -}); diff --git a/x-pack/legacy/plugins/reporting/public/views/management/jobs.html b/x-pack/legacy/plugins/reporting/public/views/management/jobs.html deleted file mode 100644 index 5471513d64d95..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/jobs.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
\ No newline at end of file diff --git a/x-pack/legacy/plugins/reporting/public/views/management/jobs.js b/x-pack/legacy/plugins/reporting/public/views/management/jobs.js deleted file mode 100644 index 7205fad8cca53..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/jobs.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import routes from 'ui/routes'; -import template from 'plugins/reporting/views/management/jobs.html'; - -import { ReportListing } from '../../components/report_listing'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; - -const REACT_ANCHOR_DOM_ELEMENT_ID = 'reportListingAnchor'; - -routes.when('/management/kibana/reporting', { - template, - k7Breadcrumbs: () => [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.reporting.breadcrumb', { - defaultMessage: 'Reporting', - }), - }, - ], - controllerAs: 'jobsCtrl', - controller($scope, kbnUrl) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); - - $scope.$on('$destroy', () => { - const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); - if (node) { - unmountComponentAtNode(node); - } - }); - }, -}); diff --git a/x-pack/legacy/plugins/reporting/public/views/management/management.js b/x-pack/legacy/plugins/reporting/public/views/management/management.js deleted file mode 100644 index 8643e6fa8b8b4..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/management.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { management } from 'ui/management'; -import { i18n } from '@kbn/i18n'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import 'plugins/reporting/views/management/jobs'; - -routes.defaults(/\/management/, { - resolve: { - reportingManagementSection: function() { - const kibanaManagementSection = management.getSection('kibana'); - const showReportingLinks = xpackInfo.get('features.reporting.management.showLinks'); - - kibanaManagementSection.deregister('reporting'); - if (showReportingLinks) { - const enableReportingLinks = xpackInfo.get('features.reporting.management.enableLinks'); - const tooltipMessage = xpackInfo.get('features.reporting.management.message'); - - let url; - let tooltip; - if (enableReportingLinks) { - url = '#/management/kibana/reporting'; - } else { - tooltip = tooltipMessage; - } - - return kibanaManagementSection.register('reporting', { - order: 15, - display: i18n.translate('xpack.reporting.management.reportingTitle', { - defaultMessage: 'Reporting', - }), - url, - tooltip, - }); - } - }, - }, -}); diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index b4d49fd21f230..917e9d7daae40 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -23,22 +23,6 @@ export type Job = EventEmitter & { }; }; -export interface ReportingConfigOptions { - browser: BrowserConfig; - poll: { - jobCompletionNotifier: { - interval: number; - intervalErrorMultiplier: number; - }; - jobsRefresh: { - interval: number; - intervalErrorMultiplier: number; - }; - }; - queue: QueueConfig; - capture: CaptureConfig; -} - export interface NetworkPolicyRule { allow: boolean; protocol: string; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js index 024001d463240..c3996fe3231b1 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js @@ -26,14 +26,14 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { CronEditor } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/cron_editor'; -import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from '../../../../legacy_imports'; +import { indexPatterns } from '../../../../../../../../../src/plugins/data/public'; + +import { indices } from '../../../../shared_imports'; import { getLogisticalDetailsUrl, getCronUrl } from '../../../services'; import { StepError } from './components'; -import { indexPatterns } from '../../../../../../../../../src/plugins/data/public'; - const indexPatternIllegalCharacters = indexPatterns.ILLEGAL_CHARACTERS_VISIBLE.join(' '); -const indexIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); +const indexIllegalCharacters = indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); export class StepLogistics extends Component { static propTypes = { diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js index 637caa2199c42..ac4bacc291ea3 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { findIllegalCharactersInIndexName } from '../../../../legacy_imports'; +import { indices } from '../../../../shared_imports'; export function validateRollupIndex(rollupIndex, indexPattern) { if (!rollupIndex || !rollupIndex.trim()) { @@ -27,7 +27,7 @@ export function validateRollupIndex(rollupIndex, indexPattern) { ]; } - const illegalCharacters = findIllegalCharactersInIndexName(rollupIndex); + const illegalCharacters = indices.findIllegalCharactersInIndexName(rollupIndex); if (illegalCharacters.length) { return [ diff --git a/x-pack/legacy/plugins/rollup/public/legacy_imports.ts b/x-pack/legacy/plugins/rollup/public/legacy_imports.ts index 07155a4b0a60e..85fa3022f59ed 100644 --- a/x-pack/legacy/plugins/rollup/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/rollup/public/legacy_imports.ts @@ -4,8 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -export { findIllegalCharactersInIndexName, INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; - export { AggTypeFilters } from 'ui/agg_types'; export { AggTypeFieldFilters } from 'ui/agg_types'; diff --git a/x-pack/legacy/plugins/reporting/public/views/management/index.js b/x-pack/legacy/plugins/rollup/public/shared_imports.ts similarity index 76% rename from x-pack/legacy/plugins/reporting/public/views/management/index.js rename to x-pack/legacy/plugins/rollup/public/shared_imports.ts index 0ed6fe09ef80a..6bf74da6db6fe 100644 --- a/x-pack/legacy/plugins/reporting/public/views/management/index.js +++ b/x-pack/legacy/plugins/rollup/public/shared_imports.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './management'; +export { indices } from '../../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts index e58bc95b9a375..e45713e2b807c 100644 --- a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts +++ b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts @@ -127,7 +127,7 @@ export function registerJobsRoute(deps: RouteDependencies, legacy: ServerShim) { { id: schema.string(), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), }), }, diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts index 1624586d4ca14..de17f40a3ac71 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts @@ -5,12 +5,14 @@ */ import { NUMBER_OF_SIGNALS, + OPEN_CLOSE_SIGNALS_BTN, SELECTED_SIGNALS, SHOWING_SIGNALS, SIGNALS, } from '../screens/detections'; import { + closeFirstSignal, closeSignals, goToClosedSignals, goToOpenedSignals, @@ -26,7 +28,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; describe('Detections', () => { - before(() => { + beforeEach(() => { esArchiverLoad('signals'); loginAndWaitForPage(DETECTIONS); }); @@ -53,6 +55,7 @@ describe('Detections', () => { waitForSignals(); cy.reload(); waitForSignals(); + waitForSignalsToBeLoaded(); const expectedNumberOfSignalsAfterClosing = +numberOfSignals - numberOfSignalsToBeClosed; cy.get(NUMBER_OF_SIGNALS) @@ -111,4 +114,43 @@ describe('Detections', () => { .should('eql', expectedNumberOfOpenedSignals.toString()); }); }); + + it('Closes one signal when more than one opened signals are selected', () => { + waitForSignalsToBeLoaded(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .then(numberOfSignals => { + const numberOfSignalsToBeClosed = 1; + const numberOfSignalsToBeSelected = 3; + + cy.get(OPEN_CLOSE_SIGNALS_BTN).should('have.attr', 'disabled'); + selectNumberOfSignals(numberOfSignalsToBeSelected); + cy.get(OPEN_CLOSE_SIGNALS_BTN).should('not.have.attr', 'disabled'); + + closeFirstSignal(); + cy.reload(); + waitForSignalsToBeLoaded(); + waitForSignals(); + + const expectedNumberOfSignals = +numberOfSignals - numberOfSignalsToBeClosed; + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eq', expectedNumberOfSignals.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${expectedNumberOfSignals.toString()} signals`); + + goToClosedSignals(); + waitForSignals(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eql', numberOfSignalsToBeClosed.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signal`); + cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts index 8c384c9010665..ce73fe1b7c2a5 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -7,30 +7,30 @@ import { newRule } from '../objects/rule'; import { - ABOUT_DESCRIPTION, - ABOUT_EXPECTED_URLS, ABOUT_FALSE_POSITIVES, ABOUT_MITRE, ABOUT_RISK, - ABOUT_RULE_DESCRIPTION, ABOUT_SEVERITY, + ABOUT_STEP, ABOUT_TAGS, ABOUT_TIMELINE, + ABOUT_URLS, DEFINITION_CUSTOM_QUERY, - DEFINITION_DESCRIPTION, DEFINITION_INDEX_PATTERNS, + DEFINITION_STEP, RULE_NAME_HEADER, - SCHEDULE_DESCRIPTION, SCHEDULE_LOOPBACK, SCHEDULE_RUNS, + SCHEDULE_STEP, + ABOUT_RULE_DESCRIPTION, } from '../screens/rule_details'; import { CUSTOM_RULES_BTN, ELASTIC_RULES_BTN, RISK_SCORE, RULE_NAME, - RULES_TABLE, RULES_ROW, + RULES_TABLE, SEVERITY, } from '../screens/signal_detection_rules'; @@ -127,10 +127,25 @@ describe('Signal detection rules', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER) - .invoke('text') - .should('eql', `${newRule.name} Beta`); - + let expectedUrls = ''; + newRule.referenceUrls.forEach(url => { + expectedUrls = expectedUrls + url; + }); + let expectedFalsePositives = ''; + newRule.falsePositivesExamples.forEach(falsePositive => { + expectedFalsePositives = expectedFalsePositives + falsePositive; + }); + let expectedTags = ''; + newRule.tags.forEach(tag => { + expectedTags = expectedTags + tag; + }); + let expectedMitre = ''; + newRule.mitre.forEach(mitre => { + expectedMitre = expectedMitre + mitre.tactic; + mitre.techniques.forEach(technique => { + expectedMitre = expectedMitre + technique; + }); + }); const expectedIndexPatterns = [ 'apm-*-transaction*', 'auditbeat-*', @@ -139,77 +154,60 @@ describe('Signal detection rules', () => { 'packetbeat-*', 'winlogbeat-*', ]; - cy.get(DEFINITION_INDEX_PATTERNS).then(patterns => { - cy.wrap(patterns).each((pattern, index) => { - cy.wrap(pattern) - .invoke('text') - .should('eql', expectedIndexPatterns[index]); - }); - }); - cy.get(DEFINITION_DESCRIPTION) - .eq(DEFINITION_CUSTOM_QUERY) + + cy.get(RULE_NAME_HEADER) .invoke('text') - .should('eql', `${newRule.customQuery} `); - cy.get(ABOUT_DESCRIPTION) - .eq(ABOUT_RULE_DESCRIPTION) + .should('eql', `${newRule.name} Beta`); + + cy.get(ABOUT_RULE_DESCRIPTION) .invoke('text') .should('eql', newRule.description); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_SEVERITY) .invoke('text') .should('eql', newRule.severity); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_RISK) .invoke('text') .should('eql', newRule.riskScore); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_TIMELINE) .invoke('text') .should('eql', 'Default blank timeline'); - - let expectedUrls = ''; - newRule.referenceUrls.forEach(url => { - expectedUrls = expectedUrls + url; - }); - cy.get(ABOUT_DESCRIPTION) - .eq(ABOUT_EXPECTED_URLS) + cy.get(ABOUT_STEP) + .eq(ABOUT_URLS) .invoke('text') .should('eql', expectedUrls); - - let expectedFalsePositives = ''; - newRule.falsePositivesExamples.forEach(falsePositive => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_FALSE_POSITIVES) .invoke('text') .should('eql', expectedFalsePositives); - - let expectedMitre = ''; - newRule.mitre.forEach(mitre => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach(technique => { - expectedMitre = expectedMitre + technique; - }); - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_MITRE) .invoke('text') .should('eql', expectedMitre); - - let expectedTags = ''; - newRule.tags.forEach(tag => { - expectedTags = expectedTags + tag; - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_TAGS) .invoke('text') .should('eql', expectedTags); - cy.get(SCHEDULE_DESCRIPTION) + + cy.get(DEFINITION_INDEX_PATTERNS).then(patterns => { + cy.wrap(patterns).each((pattern, index) => { + cy.wrap(pattern) + .invoke('text') + .should('eql', expectedIndexPatterns[index]); + }); + }); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_CUSTOM_QUERY) + .invoke('text') + .should('eql', `${newRule.customQuery} `); + + cy.get(SCHEDULE_STEP) .eq(SCHEDULE_RUNS) .invoke('text') .should('eql', '5m'); - cy.get(SCHEDULE_DESCRIPTION) + cy.get(SCHEDULE_STEP) .eq(SCHEDULE_LOOPBACK) .invoke('text') .should('eql', '1m'); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts index 4889d40ae7d39..aca988e195161 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts @@ -49,7 +49,7 @@ describe('timeline data providers', () => { .first() .invoke('text') .should(hostname => { - expect(dataProviderText).to.eq(`host.name: "${hostname}"`); + expect(dataProviderText).to.eq(hostname); }); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts index 1a94a4abbe5bf..02da7cbc28462 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TIMELINE_FLYOUT_BODY, TIMELINE_NOT_READY_TO_DROP_BUTTON } from '../screens/timeline'; +import { TIMELINE_FLYOUT_HEADER, TIMELINE_NOT_READY_TO_DROP_BUTTON } from '../screens/timeline'; import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; import { loginAndWaitForPage } from '../tasks/login'; @@ -26,10 +26,11 @@ describe('timeline flyout button', () => { it('toggles open the timeline', () => { openTimeline(); - cy.get(TIMELINE_FLYOUT_BODY).should('have.css', 'visibility', 'visible'); + cy.get(TIMELINE_FLYOUT_HEADER).should('have.css', 'visibility', 'visible'); }); - it('sets the flyout button background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => { + // FLAKY: https://github.com/elastic/kibana/issues/60369 + it.skip('sets the flyout button background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => { dragFirstHostToTimeline(); cy.get(TIMELINE_NOT_READY_TO_DROP_BUTTON).should( diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts index 8b5ba23578807..f388ac1215d01 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -12,7 +12,9 @@ export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"]'; -export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] .siemLinkIcon__label'; +export const OPEN_CLOSE_SIGNAL_BTN = '[data-test-subj="update-signal-status-button"]'; + +export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] button'; export const OPENED_SIGNALS_BTN = '[data-test-subj="openSignals"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts index 46da52cd0ddd8..6c16735ba5f24 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts @@ -4,35 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ABOUT_DESCRIPTION = '[data-test-subj="aboutRule"] .euiDescriptionList__description'; +export const ABOUT_FALSE_POSITIVES = 4; -export const ABOUT_EXPECTED_URLS = 4; +export const ABOUT_MITRE = 5; -export const ABOUT_FALSE_POSITIVES = 5; +export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]'; -export const ABOUT_MITRE = 6; +export const ABOUT_RISK = 1; -export const ABOUT_RULE_DESCRIPTION = 0; +export const ABOUT_SEVERITY = 0; -export const ABOUT_RISK = 2; +export const ABOUT_STEP = '[data-test-subj="aboutRule"] .euiDescriptionList__description'; -export const ABOUT_SEVERITY = 1; +export const ABOUT_TAGS = 6; -export const ABOUT_TAGS = 7; +export const ABOUT_TIMELINE = 2; -export const ABOUT_TIMELINE = 3; +export const ABOUT_URLS = 3; export const DEFINITION_CUSTOM_QUERY = 1; -export const DEFINITION_DESCRIPTION = - '[data-test-subj="definition"] .euiDescriptionList__description'; - export const DEFINITION_INDEX_PATTERNS = - '[data-test-subj="definition"] .euiDescriptionList__description .euiBadge__text'; + '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description .euiBadge__text'; + +export const DEFINITION_STEP = + '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description'; export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]'; -export const SCHEDULE_DESCRIPTION = '[data-test-subj="schedule"] .euiDescriptionList__description'; +export const SCHEDULE_STEP = '[data-test-subj="schedule"] .euiDescriptionList__description'; export const SCHEDULE_RUNS = 0; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts b/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts index 5638b8d23e83a..fbce585a70f86 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts @@ -31,6 +31,8 @@ export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContain export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; +export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="eui-flyout-header"]'; + export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts index 21a0c136b90df..3416e3eb81de3 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts @@ -8,6 +8,7 @@ import { CLOSED_SIGNALS_BTN, LOADING_SIGNALS_PANEL, MANAGE_SIGNAL_DETECTION_RULES_BTN, + OPEN_CLOSE_SIGNAL_BTN, OPEN_CLOSE_SIGNALS_BTN, OPENED_SIGNALS_BTN, SIGNALS, @@ -15,6 +16,12 @@ import { } from '../screens/detections'; import { REFRESH_BUTTON } from '../screens/siem_header'; +export const closeFirstSignal = () => { + cy.get(OPEN_CLOSE_SIGNAL_BTN) + .first() + .click({ force: true }); +}; + export const closeSignals = () => { cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); }; diff --git a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js b/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js index 8ca61b2397d8b..f3a97f5b9c9b6 100644 --- a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js +++ b/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js @@ -17,6 +17,16 @@ run( [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], { fileExtensions: ['ts', 'js', 'tsx'], + excludeRegExp: [ + 'test.ts$', + 'test.tsx$', + 'containers/detection_engine/rules/types.ts$', + 'core/public/chrome/chrome_service.tsx$', + 'src/core/server/types.ts$', + 'src/core/server/saved_objects/types.ts$', + 'src/core/public/overlays/banners/banners_service.tsx$', + 'src/core/public/saved_objects/saved_objects_client.ts$', + ], } ); diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index ad4a6e86ffc88..472a473842f02 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@types/lodash": "^4.14.110", "@types/js-yaml": "^3.12.1", - "@types/react-beautiful-dnd": "^11.0.4" + "@types/react-beautiful-dnd": "^12.1.1" }, "dependencies": { "lodash": "^4.17.15", diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx index f3b2b736ed87d..5c15f2d3c8d4f 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx @@ -16,8 +16,8 @@ import { RecursivePartial, } from '@elastic/charts'; import { getOr, get, isNull, isNumber } from 'lodash/fp'; -import useResizeObserver from 'use-resize-observer/polyfilled'; +import { useThrottledResizeObserver } from '../utils'; import { ChartPlaceHolder } from './chart_place_holder'; import { useTimeZone } from '../../lib/kibana'; import { @@ -131,7 +131,7 @@ interface AreaChartComponentProps { } export const AreaChartComponent: React.FC = ({ areaChart, configs }) => { - const { ref: measureRef, width, height } = useResizeObserver({}); + const { ref: measureRef, width, height } = useThrottledResizeObserver(); const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); const chartHeight = getChartHeight(customHeight, height); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx index da0f3d1d0047f..f53a1555fa1f4 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { Chart, BarSeries, Axis, Position, ScaleType, Settings } from '@elastic/charts'; import { getOr, get, isNumber } from 'lodash/fp'; import deepmerge from 'deepmerge'; -import useResizeObserver from 'use-resize-observer/polyfilled'; +import { useThrottledResizeObserver } from '../utils'; import { useTimeZone } from '../../lib/kibana'; import { ChartPlaceHolder } from './chart_place_holder'; import { @@ -105,7 +105,7 @@ interface BarChartComponentProps { } export const BarChartComponent: React.FC = ({ barChart, configs }) => { - const { ref: measureRef, width, height } = useResizeObserver({}); + const { ref: measureRef, width, height } = useThrottledResizeObserver(); const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); const chartHeight = getChartHeight(customHeight, height); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index 72f5a62d0af97..11db33fff6d72 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaultTo, noop } from 'lodash/fp'; +import { noop } from 'lodash/fp'; import React, { useCallback } from 'react'; import { DropResult, DragDropContext } from 'react-beautiful-dnd'; import { connect, ConnectedProps } from 'react-redux'; @@ -103,10 +103,7 @@ DragDropContextWrapperComponent.displayName = 'DragDropContextWrapperComponent'; const emptyDataProviders: dragAndDropModel.IdToDataProvider = {}; // stable reference const mapStateToProps = (state: State) => { - const dataProviders = defaultTo( - emptyDataProviders, - dragAndDropSelectors.dataProvidersSelector(state) - ); + const dataProviders = dragAndDropSelectors.dataProvidersSelector(state) ?? emptyDataProviders; return { dataProviders }; }; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index 9dcc335d4ff16..11891afabbf3d 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -88,21 +88,9 @@ describe('DraggableWrapper', () => { describe('ConditionalPortal', () => { const mount = useMountAppended(); const props = { - usePortal: false, registerProvider: jest.fn(), - isDragging: true, }; - it(`doesn't call registerProvider is NOT isDragging`, () => { - mount( - -
- - ); - - expect(props.registerProvider.mock.calls.length).toEqual(0); - }); - it('calls registerProvider when isDragging', () => { mount( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index b7d368639ed92..3a6a4de7984db 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Draggable, DraggableProvided, @@ -15,7 +15,6 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import { EuiPortal } from '@elastic/eui'; import { dragAndDropActions } from '../../store/drag_and_drop'; import { DataProvider } from '../timeline/data_providers/data_provider'; import { TruncatableText } from '../truncatable_text'; @@ -27,9 +26,6 @@ export const DragEffects = styled.div``; DragEffects.displayName = 'DragEffects'; -export const DraggablePortalContext = createContext(false); -export const useDraggablePortalContext = () => useContext(DraggablePortalContext); - /** * Wraps the `react-beautiful-dnd` error boundary. See also: * https://github.com/atlassian/react-beautiful-dnd/blob/v12.0.0/docs/guides/setup-problem-detection-and-error-recovery.md @@ -89,7 +85,6 @@ export const DraggableWrapper = React.memo( ({ dataProvider, render, truncate }) => { const [providerRegistered, setProviderRegistered] = useState(false); const dispatch = useDispatch(); - const usePortal = useDraggablePortalContext(); const registerProvider = useCallback(() => { if (!providerRegistered) { @@ -113,7 +108,26 @@ export const DraggableWrapper = React.memo( return ( - + ( + +
+ + {render(dataProvider, provided, snapshot)} + +
+
+ )} + > {droppableProvided => (
( key={getDraggableId(dataProvider.id)} > {(provided, snapshot) => ( - - - {truncate && !snapshot.isDragging ? ( - - {render(dataProvider, provided, snapshot)} - - ) : ( - - {render(dataProvider, provided, snapshot)} - - )} - - + {truncate && !snapshot.isDragging ? ( + + {render(dataProvider, provided, snapshot)} + + ) : ( + + {render(dataProvider, provided, snapshot)} + + )} + )} {droppableProvided.placeholder} @@ -178,20 +183,16 @@ DraggableWrapper.displayName = 'DraggableWrapper'; interface ConditionalPortalProps { children: React.ReactNode; - usePortal: boolean; - isDragging: boolean; registerProvider: () => void; } export const ConditionalPortal = React.memo( - ({ children, usePortal, registerProvider, isDragging }) => { + ({ children, registerProvider }) => { useEffect(() => { - if (isDragging) { - registerProvider(); - } - }, [isDragging, registerProvider]); + registerProvider(); + }, [registerProvider]); - return usePortal ? {children} : <>{children}; + return <>{children}; } ); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx index 821ef9be10e8d..a81954f57564e 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx @@ -6,7 +6,7 @@ import { rgba } from 'polished'; import React from 'react'; -import { Droppable } from 'react-beautiful-dnd'; +import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; import styled from 'styled-components'; interface Props { @@ -16,6 +16,7 @@ interface Props { isDropDisabled?: boolean; type?: string; render?: ({ isDraggingOver }: { isDraggingOver: boolean }) => React.ReactNode; + renderClone?: DraggableChildrenFn; } const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string }>` @@ -94,12 +95,14 @@ export const DroppableWrapper = React.memo( isDropDisabled = false, type, render = null, + renderClone, }) => ( {(provided, snapshot) => ( (({ width }) => { + if (width) { + return { + style: { + width: `${width}px`, + }, + }; + } +})` background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; border: ${({ theme }) => theme.eui.euiBorderThin}; box-shadow: 0 2px 2px -1px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)}, @@ -24,12 +36,9 @@ Field.displayName = 'Field'; * Renders a field (e.g. `event.action`) as a draggable badge */ -// Passing the styles directly to the component because the width is -// being calculated and is recommended by Styled Components for performance -// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 -export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: string }>( +export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: number }>( ({ fieldId, fieldWidth }) => ( - + {fieldId} ) diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx index 57f047416ec1c..1fe6c936d2823 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiToolTip, IconType } from '@elastic/eui'; +import { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; import React from 'react'; +import styled from 'styled-components'; import { Omit } from '../../../common/utility_types'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; @@ -116,13 +117,9 @@ export const DefaultDraggable = React.memo( DefaultDraggable.displayName = 'DefaultDraggable'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -export const Badge = (props: EuiBadgeProps) => ( - -); +export const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx index 249aae1eda0eb..15c423a3b3dc1 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx @@ -12,7 +12,6 @@ import { EuiOutsideClickDetector, } from '@elastic/eui'; import { FeatureGeometry, FeatureProperty, MapToolTipProps } from '../types'; -import { DraggablePortalContext } from '../../drag_and_drop/draggable_wrapper'; import { ToolTipFooter } from './tooltip_footer'; import { LineToolTipContent } from './line_tool_tip_content'; import { PointToolTipContent } from './point_tool_tip_content'; @@ -101,46 +100,44 @@ export const MapToolTipComponent = ({ ) : ( - - { - if (closeTooltip != null) { - closeTooltip(); - setFeatureIndex(0); - } - }} - > -
- {featureGeometry != null && featureGeometry.type === 'LineString' ? ( - - ) : ( - - )} - {features.length > 1 && ( - { - setFeatureIndex(featureIndex - 1); - setIsLoadingNextFeature(true); - }} - nextFeature={() => { - setFeatureIndex(featureIndex + 1); - setIsLoadingNextFeature(true); - }} - /> - )} - {isLoadingNextFeature && } -
-
-
+ { + if (closeTooltip != null) { + closeTooltip(); + setFeatureIndex(0); + } + }} + > +
+ {featureGeometry != null && featureGeometry.type === 'LineString' ? ( + + ) : ( + + )} + {features.length > 1 && ( + { + setFeatureIndex(featureIndex - 1); + setIsLoadingNextFeature(true); + }} + nextFeature={() => { + setFeatureIndex(featureIndex + 1); + setIsLoadingNextFeature(true); + }} + /> + )} + {isLoadingNextFeature && } +
+
); }; diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx index e9903ce66d799..cd94a9fdcb5ac 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx @@ -115,6 +115,17 @@ export const getColumns = ({ )} isDropDisabled={true} type={DRAG_TYPE_FIELD} + renderClone={provided => ( +
+ + + +
+ )} > - {(provided, snapshot) => ( + {provided => (
- {!snapshot.isDragging ? ( - - ) : ( - - - - )} +
)}
diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx new file mode 100644 index 0000000000000..86a776a0313cc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { createContext, useContext } from 'react'; +import { useThrottledResizeObserver } from '../utils'; + +const EventDetailsWidthContext = createContext(0); + +export const useEventDetailsWidthContext = () => useContext(EventDetailsWidthContext); + +export const EventDetailsWidthProvider = React.memo(({ children }) => { + const { ref, width } = useThrottledResizeObserver(); + + return ( + <> + + {children} + +
+ + ); +}); + +EventDetailsWidthProvider.displayName = 'EventDetailsWidthProvider'; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index a913186d9ad3b..ea2cb661763fa 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -9,7 +9,6 @@ import { getOr, isEmpty, union } from 'lodash/fp'; import React, { useMemo } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import useResizeObserver from 'use-resize-observer/polyfilled'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; @@ -25,8 +24,8 @@ import { OnChangeItemsPerPage } from '../timeline/events'; import { Footer, footerHeight } from '../timeline/footer'; import { combineQueries } from '../timeline/helpers'; import { TimelineRefetch } from '../timeline/refetch_timeline'; -import { isCompactFooter } from '../timeline/timeline'; import { ManageTimelineContext, TimelineTypeContextProps } from '../timeline/timeline_context'; +import { EventDetailsWidthProvider } from './event_details_width_context'; import * as i18n from './translations'; import { Filter, @@ -38,15 +37,15 @@ import { inputsModel } from '../../store'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; -const WrappedByAutoSizer = styled.div` - width: 100%; -`; // required by AutoSizer -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; - const StyledEuiPanel = styled(EuiPanel)` max-width: 100%; `; +const EventsContainerLoading = styled.div` + width: 100%; + overflow: auto; +`; + interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; @@ -94,7 +93,6 @@ const EventsViewerComponent: React.FC = ({ toggleColumn, utilityBar, }) => { - const { ref: measureRef, width = 0 } = useResizeObserver({}); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); const combinedQueries = combineQueries({ @@ -117,25 +115,25 @@ const EventsViewerComponent: React.FC = ({ ), [columnsHeader, timelineTypeContext.queryFields] ); + const sortField = useMemo( + () => ({ + sortFieldId: sort.columnId, + direction: sort.sortDirection as Direction, + }), + [sort.columnId, sort.sortDirection] + ); return ( - <> - -
- - - {combinedQueries != null ? ( + {combinedQueries != null ? ( + {({ @@ -169,15 +167,8 @@ const EventsViewerComponent: React.FC = ({ {utilityBar?.(refetch, totalCountMinusDeleted)} -
- + + = ({ />
= ({ tieBreaker={getOr(null, 'endCursor.tiebreaker', pageInfo)} /> -
+ ); }}
- ) : null} - +
+ ) : null} ); }; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx index 990c2678b1006..62f9297c38ef5 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx @@ -90,6 +90,13 @@ export const getFieldItems = ({ key={`field-browser-field-items-field-droppable-wrapper-${timelineId}-${categoryId}-${field.name}`} isDropDisabled={true} type={DRAG_TYPE_FIELD} + renderClone={provided => ( +
+ + + +
+ )} > - {(provided, snapshot) => ( -
- {!snapshot.isDragging ? ( - - - - c.id === field.name) !== -1} - data-test-subj={`field-${field.name}-checkbox`} - id={field.name || ''} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field.name || '', - width: DEFAULT_COLUMN_MIN_WIDTH, - }) - } - /> - - - - - - - - + {provided => ( +
+ + + + c.id === field.name) !== -1} + data-test-subj={`field-${field.name}-checkbox`} + id={field.name || ''} + onChange={() => + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field.name || '', + width: DEFAULT_COLUMN_MIN_WIDTH, + }) + } + /> + + - - + + - - - ) : ( - - - - )} + + + + + + +
)} diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap index abdc4f4681294..4bf0033bcb430 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap @@ -3,7 +3,6 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` ( isDataInTimeline, isDatepickerLocked, title, - width = DEFAULT_TIMELINE_WIDTH, noteIds, notesById, timelineId, @@ -77,7 +75,6 @@ const StatefulFlyoutHeader = React.memo( updateTitle={updateTitle} updateNote={updateNote} usersViewing={usersViewing} - width={width} /> ); } @@ -103,7 +100,6 @@ const makeMapStateToProps = () => { kqlQuery, title = '', noteIds = emptyNotesId, - width = DEFAULT_TIMELINE_WIDTH, } = timeline; const history = emptyHistory; // TODO: get history from store via selector @@ -118,7 +114,6 @@ const makeMapStateToProps = () => { isDatepickerLocked: globalInput.linkTo.includes('timeline'), noteIds, title, - width, }; }; return mapStateToProps; @@ -126,28 +121,6 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), - applyDeltaToWidth: ({ - id, - delta, - bodyClientWidthPixels, - maxWidthPercent, - minWidthPixels, - }: { - id: string; - delta: number; - bodyClientWidthPixels: number; - maxWidthPercent: number; - minWidthPixels: number; - }) => - dispatch( - timelineActions.applyDeltaToWidth({ - id, - delta, - bodyClientWidthPixels, - maxWidthPercent, - minWidthPixels, - }) - ), createTimeline: ({ id, show }: { id: string; show?: boolean }) => dispatch( timelineActions.createTimeline({ diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..df96f2a1f7eba --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FlyoutHeaderWithCloseButton renders correctly against snapshot 1`] = ` + +`; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx new file mode 100644 index 0000000000000..e0eace2ad5b10 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../mock'; +import { FlyoutHeaderWithCloseButton } from '.'; + +describe('FlyoutHeaderWithCloseButton', () => { + test('renders correctly against snapshot', () => { + const EmptyComponent = shallow( + + + + ); + expect(EmptyComponent.find('FlyoutHeaderWithCloseButton')).toMatchSnapshot(); + }); + + test('it should invoke onClose when the close button is clicked', () => { + const closeMock = jest.fn(); + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="close-timeline"] button') + .first() + .simulate('click'); + + expect(closeMock).toBeCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx new file mode 100644 index 0000000000000..a4d9f0e8293df --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import styled from 'styled-components'; + +import { FlyoutHeader } from '../header'; +import * as i18n from './translations'; + +const FlyoutHeaderContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +`; + +// manually wrap the close button because EuiButtonIcon can't be a wrapped `styled` +const WrappedCloseButton = styled.div` + margin-right: 5px; +`; + +const FlyoutHeaderWithCloseButtonComponent: React.FC<{ + onClose: () => void; + timelineId: string; + usersViewing: string[]; +}> = ({ onClose, timelineId, usersViewing }) => ( + + + + + + + + +); + +export const FlyoutHeaderWithCloseButton = React.memo(FlyoutHeaderWithCloseButtonComponent); + +FlyoutHeaderWithCloseButton.displayName = 'FlyoutHeaderWithCloseButton'; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/docs_links.ts b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts similarity index 55% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/lib/docs_links.ts rename to x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts index 761fcd2674df6..7fcffc9c1f0b4 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/docs_links.ts +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -let docLinks: Record = {}; +import { i18n } from '@kbn/i18n'; -export const setDocLinks = (links: Record) => { - docLinks = links; -}; - -export const getDocLinks = () => docLinks; +export const CLOSE_TIMELINE = i18n.translate( + 'xpack.siem.timeline.flyout.header.closeTimelineButtonLabel', + { + defaultMessage: 'Close timeline', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index 83b842956e10e..ab41b4617894e 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -13,9 +13,14 @@ import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mo import { createStore, State } from '../../store'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; -import { Flyout, FlyoutComponent, flyoutHeaderHeight } from '.'; +import { Flyout, FlyoutComponent } from '.'; import { FlyoutButton } from './button'; +jest.mock('../timeline', () => ({ + // eslint-disable-next-line react/display-name + StatefulTimeline: () =>
, +})); + const testFlyoutHeight = 980; const usersViewing = ['elastic']; @@ -26,12 +31,7 @@ describe('Flyout', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper.find('Flyout')).toMatchSnapshot(); @@ -40,12 +40,7 @@ describe('Flyout', () => { test('it renders the default flyout state as a button', () => { const wrapper = mount( - + ); @@ -57,41 +52,13 @@ describe('Flyout', () => { ).toContain('Timeline'); }); - test('it renders the title field when its state is set to flyout is true', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="timeline-title"]') - .first() - .props().placeholder - ).toContain('Untitled Timeline'); - }); - test('it does NOT render the fly out button when its state is set to flyout is true', () => { const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); const wrapper = mount( - + ); @@ -100,31 +67,6 @@ describe('Flyout', () => { ); }); - test('it renders the flyout body', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); - - const wrapper = mount( - - -

{'Fake flyout body'}

-
-
- ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout-body"]') - .first() - .text() - ).toContain('Fake flyout body'); - }); - test('it does render the data providers badge when the number is greater than 0', () => { const stateWithDataProviders = set( 'timeline.timelineById.test.dataProviders', @@ -135,12 +77,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -157,12 +94,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -177,12 +109,7 @@ describe('Flyout', () => { test('it hides the data providers badge when the timeline does NOT have data providers', () => { const wrapper = mount( - + ); @@ -204,12 +131,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -228,7 +150,6 @@ describe('Flyout', () => { { expect(showTimeline).toBeCalled(); }); - - test('should call the onClose when the close button is clicked', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); - - const showTimeline = (jest.fn() as unknown) as ActionCreator<{ id: string; show: boolean }>; - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="close-timeline"] button') - .first() - .simulate('click'); - - expect(showTimeline).toBeCalled(); - }); }); describe('showFlyoutButton', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index 22fc9f27ce26c..44abe5b679c8e 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -5,7 +5,6 @@ */ import { EuiBadge } from '@elastic/eui'; -import { defaultTo, getOr } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; @@ -16,9 +15,8 @@ import { FlyoutButton } from './button'; import { Pane } from './pane'; import { timelineActions } from '../../store/actions'; import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; - -/** The height in pixels of the flyout header, exported for use in height calculations */ -export const flyoutHeaderHeight: number = 60; +import { StatefulTimeline } from '../timeline'; +import { TimelineById } from '../../store/timeline/types'; export const Badge = styled(EuiBadge)` position: absolute; @@ -38,9 +36,7 @@ const Visible = styled.div<{ show?: boolean }>` Visible.displayName = 'Visible'; interface OwnProps { - children?: React.ReactNode; flyoutHeight: number; - headerHeight: number; timelineId: string; usersViewing: string[]; } @@ -48,17 +44,7 @@ interface OwnProps { type Props = OwnProps & ProsFromRedux; export const FlyoutComponent = React.memo( - ({ - children, - dataProviders, - flyoutHeight, - headerHeight, - show, - showTimeline, - timelineId, - usersViewing, - width, - }) => { + ({ dataProviders, flyoutHeight, show, showTimeline, timelineId, usersViewing, width }) => { const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ showTimeline, timelineId, @@ -73,17 +59,15 @@ export const FlyoutComponent = React.memo( - {children} + ( FlyoutComponent.displayName = 'FlyoutComponent'; +const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; +const DEFAULT_TIMELINE_BY_ID = {}; + const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timelineById = defaultTo({}, timelineSelectors.timelineByIdSelector(state)); - const dataProviders = getOr([], `${timelineId}.dataProviders`, timelineById) as DataProvider[]; - const show = getOr(false, `${timelineId}.show`, timelineById) as boolean; - const width = getOr(DEFAULT_TIMELINE_WIDTH, `${timelineId}.width`, timelineById) as number; + const timelineById: TimelineById = + timelineSelectors.timelineByIdSelector(state) ?? DEFAULT_TIMELINE_BY_ID; + /* + In case timelineById[timelineId]?.dataProviders is an empty array it will cause unnecessary rerender + of StatefulTimeline which can be expensive, so to avoid that return DEFAULT_DATA_PROVIDERS + */ + const dataProviders = timelineById[timelineId]?.dataProviders.length + ? timelineById[timelineId]?.dataProviders + : DEFAULT_DATA_PROVIDERS; + const show = timelineById[timelineId]?.show ?? false; + const width = timelineById[timelineId]?.width ?? DEFAULT_TIMELINE_WIDTH; return { dataProviders, show, width }; }; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap index efa682cd4d18e..d30fd6f31012c 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -3,14 +3,8 @@ exports[`Pane renders correctly against snapshot 1`] = ` diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 365f99c6667b8..53cf8f95de0ce 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -8,12 +8,10 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../mock'; -import { flyoutHeaderHeight } from '..'; import { Pane } from '.'; const testFlyoutHeight = 980; const testWidth = 640; -const usersViewing = ['elastic']; describe('Pane', () => { test('renders correctly against snapshot', () => { @@ -21,10 +19,8 @@ describe('Pane', () => { {'I am a child of flyout'} @@ -39,10 +35,8 @@ describe('Pane', () => { {'I am a child of flyout'} @@ -53,87 +47,13 @@ describe('Pane', () => { expect(wrapper.find('Resizable').get(0).props.maxWidth).toEqual('95vw'); }); - test('it applies timeline styles to the EuiFlyout', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout"]') - .first() - .hasClass('timeline-flyout') - ).toEqual(true); - }); - - test('it applies timeline styles to the EuiFlyoutHeader', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout-header"]') - .first() - .hasClass('timeline-flyout-header') - ).toEqual(true); - }); - - test('it applies timeline styles to the EuiFlyoutBody', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout-body"]') - .first() - .hasClass('timeline-flyout-body') - ).toEqual(true); - }); - test('it should render a resize handle', () => { const wrapper = mount( {'I am a child of flyout'} @@ -149,74 +69,19 @@ describe('Pane', () => { ).toEqual(true); }); - test('it should render an empty title', () => { + test('it should render children', () => { const wrapper = mount( - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="timeline-title"]') - .first() - .text() - ).toContain(''); - }); - - test('it should render the flyout body', () => { - const wrapper = mount( - - {'I am a mock body'} ); - expect( - wrapper - .find('[data-test-subj="eui-flyout-body"]') - .first() - .text() - ).toContain('I am a mock body'); - }); - - test('it should invoke onClose when the close button is clicked', () => { - const closeMock = jest.fn(); - const wrapper = mount( - - - {'I am a mock child'} - - - ); - wrapper - .find('[data-test-subj="close-timeline"] button') - .first() - .simulate('click'); - - expect(closeMock).toBeCalled(); + expect(wrapper.first().text()).toContain('I am a mock body'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index 38ec4a4b6f1f3..3b5041c1ee346 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -4,130 +4,85 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useState } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { EuiFlyout } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { Resizable, ResizeCallback } from 're-resizable'; -import { throttle } from 'lodash/fp'; import { TimelineResizeHandle } from './timeline_resize_handle'; -import { FlyoutHeader } from '../header'; +import { EventDetailsWidthProvider } from '../../events_viewer/event_details_width_context'; import * as i18n from './translations'; import { timelineActions } from '../../../store/actions'; const minWidthPixels = 550; // do not allow the flyout to shrink below this width (pixels) const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view -interface OwnProps { +interface FlyoutPaneComponentProps { children: React.ReactNode; flyoutHeight: number; - headerHeight: number; onClose: () => void; timelineId: string; - usersViewing: string[]; width: number; } -type Props = OwnProps & PropsFromRedux; - -const EuiFlyoutContainer = styled.div<{ headerHeight: number }>` +const EuiFlyoutContainer = styled.div` .timeline-flyout { min-width: 150px; width: auto; } - .timeline-flyout-header { - align-items: center; - box-shadow: none; - display: flex; - flex-direction: row; - height: ${({ headerHeight }) => `${headerHeight}px`}; - max-height: ${({ headerHeight }) => `${headerHeight}px`}; - overflow: hidden; - padding: 5px 0 0 10px; - } - .timeline-flyout-body { - overflow-y: hidden; - padding: 0; - .euiFlyoutBody__overflowContent { - padding: 0; - } - } `; -const FlyoutHeaderContainer = styled.div` - align-items: center; +const StyledResizable = styled(Resizable)` display: flex; - flex-direction: row; - justify-content: space-between; - width: 100%; -`; - -// manually wrap the close button because EuiButtonIcon can't be a wrapped `styled` -const WrappedCloseButton = styled.div` - margin-right: 5px; + flex-direction: column; `; -const FlyoutHeaderWithCloseButtonComponent: React.FC<{ - onClose: () => void; - timelineId: string; - usersViewing: string[]; -}> = ({ onClose, timelineId, usersViewing }) => ( - - - - - - - - -); - -const FlyoutHeaderWithCloseButton = React.memo( - FlyoutHeaderWithCloseButtonComponent, - (prevProps, nextProps) => - prevProps.timelineId === nextProps.timelineId && - prevProps.usersViewing === nextProps.usersViewing -); +const RESIZABLE_ENABLE = { left: true }; -const FlyoutPaneComponent: React.FC = ({ - applyDeltaToWidth, +const FlyoutPaneComponent: React.FC = ({ children, flyoutHeight, - headerHeight, onClose, timelineId, - usersViewing, width, }) => { - const [lastDelta, setLastDelta] = useState(0); + const dispatch = useDispatch(); + const onResizeStop: ResizeCallback = useCallback( (e, direction, ref, delta) => { const bodyClientWidthPixels = document.body.clientWidth; if (delta.width) { - applyDeltaToWidth({ - bodyClientWidthPixels, - delta: -(delta.width - lastDelta), - id: timelineId, - maxWidthPercent, - minWidthPixels, - }); - setLastDelta(delta.width); + dispatch( + timelineActions.applyDeltaToWidth({ + bodyClientWidthPixels, + delta: -delta.width, + id: timelineId, + maxWidthPercent, + minWidthPixels, + }) + ); } }, - [applyDeltaToWidth, maxWidthPercent, minWidthPixels, lastDelta] + [dispatch] + ); + const resizableDefaultSize = useMemo( + () => ({ + width, + height: '100%', + }), + [] + ); + const resizableHandleComponent = useMemo( + () => ({ + left: , + }), + [flyoutHeight] ); - const resetLastDelta = useCallback(() => setLastDelta(0), [setLastDelta]); - const throttledResize = throttle(100, onResizeStop); return ( - + = ({ onClose={onClose} size="l" > - - ), - }} - onResizeStart={resetLastDelta} - onResize={throttledResize} + handleComponent={resizableHandleComponent} + onResizeStop={onResizeStop} > - - - - - {children} - - + {children} + ); }; -const mapDispatchToProps = { - applyDeltaToWidth: timelineActions.applyDeltaToWidth, -}; - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const Pane = connector(React.memo(FlyoutPaneComponent)); +export const Pane = React.memo(FlyoutPaneComponent); Pane.displayName = 'Pane'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts b/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts index 4ba0307eb527b..0c31cdb81e8e1 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts @@ -12,10 +12,3 @@ export const TIMELINE_DESCRIPTION = i18n.translate( defaultMessage: 'Timeline Properties', } ); - -export const CLOSE_TIMELINE = i18n.translate( - 'xpack.siem.timeline.flyout.pane.closeTimelineButtonLabel', - { - defaultMessage: 'Close timeline', - } -); diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx index d6f8143745356..f10a740db2b93 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx @@ -7,11 +7,10 @@ import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; import { getOr, omit } from 'lodash/fp'; import React, { useCallback } from 'react'; -import { connect } from 'react-redux'; -import { ActionCreator } from 'typescript-fsa'; +import { connect, ConnectedProps } from 'react-redux'; import styled, { css } from 'styled-components'; -import { inputsModel, inputsSelectors, State } from '../../store'; +import { inputsSelectors, State } from '../../store'; import { InputsModelId } from '../../store/inputs/constants'; import { inputsActions } from '../../store/inputs'; @@ -60,24 +59,7 @@ interface OwnProps { title: string | React.ReactElement | React.ReactNode; } -interface InspectButtonReducer { - id: string; - isInspected: boolean; - loading: boolean; - inspect: inputsModel.InspectQuery | null; - selectedInspectIndex: number; -} - -interface InspectButtonDispatch { - setIsInspected: ActionCreator<{ - id: string; - inputId: InputsModelId; - isInspected: boolean; - selectedInspectIndex: number; - }>; -} - -type InspectButtonProps = OwnProps & InspectButtonReducer & InspectButtonDispatch; +type InspectButtonProps = OwnProps & PropsFromRedux; const InspectButtonComponent: React.FC = ({ compact = false, @@ -175,7 +157,8 @@ const mapDispatchToProps = { setIsInspected: inputsActions.setInspectionParameter, }; -export const InspectButton = connect( - makeMapStateToProps, - mapDispatchToProps -)(React.memo(InspectButtonComponent)); +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const InspectButton = connector(React.memo(InspectButtonComponent)); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts new file mode 100644 index 0000000000000..14b367de674a2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { appendSearch } from './helpers'; + +describe('appendSearch', () => { + test('should return empty string if no parameter', () => { + expect(appendSearch()).toEqual(''); + }); + test('should return empty string if parameter is undefined', () => { + expect(appendSearch(undefined)).toEqual(''); + }); + test('should return parameter if parameter is defined', () => { + expect(appendSearch('helloWorld')).toEqual('helloWorld'); + }); +}); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/index.ts b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts similarity index 73% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/index.ts rename to x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts index 1f963d7f8fcce..9d818ab3b6479 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './boot'; +export const appendSearch = (search?: string) => (search != null ? `${search}` : ''); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx index 3701069389b72..18111aa93a27a 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { DetectionEngineTab } from '../../pages/detection_engine/types'; +import { appendSearch } from './helpers'; import { RedirectWrapper } from './redirect_wrapper'; export type DetectionEngineComponentProps = RouteComponentProps<{ @@ -63,9 +64,10 @@ export const RedirectToEditRulePage = ({ const baseDetectionEngineUrl = `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`; -export const getDetectionEngineUrl = () => `${baseDetectionEngineUrl}`; -export const getDetectionEngineAlertUrl = () => - `${baseDetectionEngineUrl}/${DetectionEngineTab.alerts}`; +export const getDetectionEngineUrl = (search?: string) => + `${baseDetectionEngineUrl}${appendSearch(search)}`; +export const getDetectionEngineAlertUrl = (search?: string) => + `${baseDetectionEngineUrl}/${DetectionEngineTab.alerts}${appendSearch(search)}`; export const getDetectionEngineTabUrl = (tabPath: string) => `${baseDetectionEngineUrl}/${tabPath}`; export const getRulesUrl = () => `${baseDetectionEngineUrl}/rules`; export const getCreateRuleUrl = () => `${baseDetectionEngineUrl}/rules/create`; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx index 05139320b171d..746a959cc996a 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx @@ -7,10 +7,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { RedirectWrapper } from './redirect_wrapper'; import { HostsTableType } from '../../store/hosts/model'; import { SiemPageName } from '../../pages/home/types'; +import { appendSearch } from './helpers'; +import { RedirectWrapper } from './redirect_wrapper'; + export type HostComponentProps = RouteComponentProps<{ detailName: string; tabName: HostsTableType; @@ -44,9 +46,10 @@ export const RedirectToHostDetailsPage = ({ const baseHostsUrl = `#/link-to/${SiemPageName.hosts}`; -export const getHostsUrl = () => baseHostsUrl; +export const getHostsUrl = (search?: string) => `${baseHostsUrl}${appendSearch(search)}`; -export const getTabsOnHostsUrl = (tabName: HostsTableType) => `${baseHostsUrl}/${tabName}`; +export const getTabsOnHostsUrl = (tabName: HostsTableType, search?: string) => + `${baseHostsUrl}/${tabName}${appendSearch(search)}`; export const getHostDetailsUrl = (detailName: string) => `${baseHostsUrl}/${detailName}`; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx index f206e2f323a74..71925edd5c086 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx @@ -7,10 +7,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { RedirectWrapper } from './redirect_wrapper'; import { SiemPageName } from '../../pages/home/types'; import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; +import { appendSearch } from './helpers'; +import { RedirectWrapper } from './redirect_wrapper'; + export type NetworkComponentProps = RouteComponentProps<{ detailName?: string; flowTarget?: string; @@ -33,7 +35,7 @@ export const RedirectToNetworkPage = ({ ); const baseNetworkUrl = `#/link-to/${SiemPageName.network}`; -export const getNetworkUrl = () => baseNetworkUrl; +export const getNetworkUrl = (search?: string) => `${baseNetworkUrl}${appendSearch(search)}`; export const getIPDetailsUrl = ( detailName: string, flowTarget?: FlowTarget | FlowTargetSourceDest diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx index 1b71432b3f729..27765a4125afc 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx @@ -6,9 +6,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { RedirectWrapper } from './redirect_wrapper'; + import { SiemPageName } from '../../pages/home/types'; +import { appendSearch } from './helpers'; +import { RedirectWrapper } from './redirect_wrapper'; + export type TimelineComponentProps = RouteComponentProps<{ search: string; }>; @@ -17,4 +20,5 @@ export const RedirectToTimelinesPage = ({ location: { search } }: TimelineCompon ); -export const getTimelinesUrl = () => `#/link-to/${SiemPageName.timelines}`; +export const getTimelinesUrl = (search?: string) => + `#/link-to/${SiemPageName.timelines}${appendSearch(search)}`; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts index 9a95d93a2df70..899d108fe246d 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts @@ -10,7 +10,7 @@ import { Location } from 'history'; import { UrlInputsModel } from '../../store/inputs/model'; import { TimelineUrl } from '../../store/timeline/model'; import { CONSTANTS } from '../url_state/constants'; -import { URL_STATE_KEYS, KeyUrlState } from '../url_state/types'; +import { URL_STATE_KEYS, KeyUrlState, UrlState } from '../url_state/types'; import { replaceQueryStringInLocation, replaceStateKeyInQueryString, @@ -18,10 +18,9 @@ import { } from '../url_state/helpers'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; -import { TabNavigationProps } from './tab_navigation/types'; import { SearchNavTab } from './types'; -export const getSearch = (tab: SearchNavTab, urlState: TabNavigationProps): string => { +export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { return URL_STATE_KEYS[tab.urlKey].reduce( (myLocation: Location, urlKey: KeyUrlState) => { @@ -58,7 +57,7 @@ export const getSearch = (tab: SearchNavTab, urlState: TabNavigationProps): stri ); }, { - pathname: urlState.pathName, + pathname: '', hash: '', search: '', state: '', diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx index cebf9b90656ca..ab4d75a2b1168 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -66,7 +66,9 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { () => Object.values(navTabs).map(tab => { const isSelected = selectedTabId === tab.id; - const hrefWithSearch = tab.href + getSearch(tab, props); + const { query, filters, savedQuery, timerange, timeline } = props; + const hrefWithSearch = + tab.href + getSearch(tab, { query, filters, savedQuery, timerange, timeline }); return ( { + const mapState = makeMapStateToProps(); + const { urlState } = useSelector(mapState, isEqual); + const urlSearch = useMemo(() => getSearch(tab, urlState), [tab, urlState]); + return urlSearch; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx index caa9cd0689c76..982937659c0aa 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonIcon, EuiModal, EuiToolTip, EuiOverlayMask } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { DeleteTimelineModal, DELETE_TIMELINE_MODAL_WIDTH } from './delete_timeline_modal'; import * as i18n from '../translations'; @@ -23,15 +23,15 @@ export const DeleteTimelineModalButton = React.memo( ({ deleteTimelines, savedObjectId, title }) => { const [showModal, setShowModal] = useState(false); - const openModal = () => setShowModal(true); - const closeModal = () => setShowModal(false); + const openModal = useCallback(() => setShowModal(true), [setShowModal]); + const closeModal = useCallback(() => setShowModal(false), [setShowModal]); - const onDelete = () => { + const onDelete = useCallback(() => { if (deleteTimelines != null && savedObjectId != null) { deleteTimelines([savedObjectId]); } closeModal(); - }; + }, [deleteTimelines, savedObjectId, closeModal]); return ( <> diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index 781155c3ddc38..ef6a19f4b7448 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -4,15 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { - EuiBadge, - EuiBadgeProps, - EuiDescriptionList, - EuiFlexGroup, - EuiIcon, - EuiPage, -} from '@elastic/eui'; +import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui'; import styled, { createGlobalStyle } from 'styled-components'; /* @@ -20,6 +12,12 @@ import styled, { createGlobalStyle } from 'styled-components'; and `EuiPopover`, `EuiToolTip` global styles */ export const AppGlobalStyle = createGlobalStyle` + /* dirty hack to fix draggables with tooltip on FF */ + body#siem-app { + position: static; + } + /* end of dirty hack to fix draggables with tooltip on FF */ + div.app-wrapper { background-color: rgba(0,0,0,0); } @@ -107,6 +105,7 @@ export const PageHeader = styled.div` PageHeader.displayName = 'PageHeader'; export const FooterContainer = styled.div` + flex: 0; bottom: 0; color: #666; left: 0; @@ -154,13 +153,9 @@ export const Pane1FlexContent = styled.div` Pane1FlexContent.displayName = 'Pane1FlexContent'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// margin-left: 5px; -// `; -export const CountBadge = (props: EuiBadgeProps) => ( - -); +export const CountBadge = styled(EuiBadge)` + margin-left: 5px; +`; CountBadge.displayName = 'CountBadge'; @@ -170,13 +165,9 @@ export const Spacer = styled.span` Spacer.displayName = 'Spacer'; -// Ref: https://github.com/elastic/eui/issues/1655 -// export const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -export const Badge = (props: EuiBadgeProps) => ( - -); +export const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx index 3868885fa29ee..52c142ceff480 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx @@ -8,7 +8,7 @@ import { isEmpty } from 'lodash/fp'; import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; +import React, { useMemo } from 'react'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; import { ESQuery } from '../../../../../common/typed_json'; @@ -23,6 +23,8 @@ import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats' import { manageQuery } from '../../../page/manage_query'; import { inputsModel } from '../../../../store/inputs'; import { InspectButtonContainer } from '../../../inspect'; +import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; +import { navTabs } from '../../../../pages/home/home_navigations'; export interface OwnProps { startDate: number; @@ -51,7 +53,15 @@ const OverviewHostComponent: React.FC = ({ setQuery, }) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - + const urlSearch = useGetUrlSearch(navTabs.hosts); + const hostPageButton = useMemo( + () => ( + + + + ), + [urlSearch] + ); return ( @@ -95,12 +105,7 @@ const OverviewHostComponent: React.FC = ({ /> } > - - - + {hostPageButton} = ({ setQuery, }) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - + const urlSearch = useGetUrlSearch(navTabs.network); + const networkPageButton = useMemo( + () => ( + + + + ), + [urlSearch] + ); return ( @@ -96,12 +106,7 @@ const OverviewNetworkComponent: React.FC = ({ /> } > - - - + {networkPageButton} ; @@ -31,14 +33,13 @@ export type Props = OwnProps & PropsFromRedux; const StatefulRecentTimelinesComponent = React.memo( ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { - const actionDispatcher = updateIsLoading as ActionCreator<{ id: string; isLoading: boolean }>; const onOpenTimeline: OnOpenTimeline = useCallback( ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { queryTimelineById({ apolloClient, duplicate, timelineId, - updateIsLoading: actionDispatcher, + updateIsLoading, updateTimeline, }); }, @@ -47,6 +48,11 @@ const StatefulRecentTimelinesComponent = React.memo( const noTimelinesMessage = filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; + const urlSearch = useGetUrlSearch(navTabs.timelines); + const linkAllTimelines = useMemo( + () => {i18n.VIEW_ALL_TIMELINES}, + [urlSearch] + ); return ( ( /> )} - - {i18n.VIEW_ALL_TIMELINES} - + {linkAllTimelines} )} diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx index 1fdcd8eee941f..0ee54a1a20003 100644 --- a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx @@ -26,16 +26,10 @@ describe('SkeletonRow', () => { expect(wrapper.find('.siemSkeletonRow__cell')).toHaveLength(10); }); - test('it applies row and cell styles when cellColor/cellMargin/rowHeight/rowPadding/style provided', () => { + test('it applies row and cell styles when cellColor/cellMargin/rowHeight/rowPadding provided', () => { const wrapper = mount( - + ); const siemSkeletonRow = wrapper.find('.siemSkeletonRow').first(); @@ -43,7 +37,6 @@ describe('SkeletonRow', () => { expect(siemSkeletonRow).toHaveStyleRule('height', '100px'); expect(siemSkeletonRow).toHaveStyleRule('padding', '10px'); - expect(siemSkeletonRow.props().style!.width).toBe('auto'); expect(siemSkeletonRowCell).toHaveStyleRule('background-color', 'red'); expect(siemSkeletonRowCell).toHaveStyleRule('margin-left', '10px', { modifier: '& + &', diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx index dce360877130e..ae30f11d8bb16 100644 --- a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx @@ -54,11 +54,10 @@ Cell.displayName = 'Cell'; export interface SkeletonRowProps extends CellProps, RowProps { cellCount?: number; - style?: object; } export const SkeletonRow = React.memo( - ({ cellColor, cellCount = 4, cellMargin, rowHeight, rowPadding, style }) => { + ({ cellColor, cellCount = 4, cellMargin, rowHeight, rowPadding }) => { const cells = useMemo( () => [...Array(cellCount)].map( @@ -69,7 +68,7 @@ export const SkeletonRow = React.memo( ); return ( - + {cells} ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap index 372930ee3167d..02938cb2b86b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -1,668 +1,702 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Timeline rendering renders correctly against snapshot 1`] = ` - - - + + + - + onChangeDataProviderKqlQuery={[MockFunction]} + onChangeDroppableAndProvider={[MockFunction]} + onDataProviderEdited={[MockFunction]} + onDataProviderRemoved={[MockFunction]} + onToggleDataProviderEnabled={[MockFunction]} + onToggleDataProviderExcluded={[MockFunction]} + show={true} + showCallOutUnauthorizedMsg={false} + /> + + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index b8b03be4e4720..03e4f4b5f0f2b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -490,7 +490,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` isCombineEnabled={false} isDropDisabled={false} mode="standard" - renderClone={null} + renderClone={[Function]} type="drag-type-field" > diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx index c3f28fd513d08..e070ed8fa1d2a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx @@ -4,27 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import { Resizable, ResizeCallback } from 're-resizable'; +import deepEqual from 'fast-deep-equal'; import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { DragEffects } from '../../../drag_and_drop/draggable_wrapper'; -import { getDraggableFieldId, DRAG_TYPE_FIELD } from '../../../drag_and_drop/helpers'; -import { DraggableFieldBadge } from '../../../draggables/field_badge'; +import { getDraggableFieldId } from '../../../drag_and_drop/helpers'; import { OnColumnRemoved, OnColumnSorted, OnFilterChange, OnColumnResized } from '../../events'; import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; import { Sort } from '../sort'; -import { DraggingContainer } from './common/dragging_container'; import { Header } from './header'; +const RESIZABLE_ENABLE = { right: true }; + interface ColumneHeaderProps { draggableIndex: number; header: ColumnHeaderOptions; onColumnRemoved: OnColumnRemoved; onColumnSorted: OnColumnSorted; onColumnResized: OnColumnResized; + isDragging: boolean; onFilterChange?: OnFilterChange; sort: Sort; timelineId: string; @@ -34,69 +35,82 @@ const ColumnHeaderComponent: React.FC = ({ draggableIndex, header, timelineId, + isDragging, onColumnRemoved, onColumnResized, onColumnSorted, onFilterChange, sort, }) => { - const [isDragging, setIsDragging] = React.useState(false); - const handleResizeStop: ResizeCallback = (e, direction, ref, delta) => { - onColumnResized({ columnId: header.id, delta: delta.width }); - }; + const resizableSize = useMemo( + () => ({ + width: header.width, + height: 'auto', + }), + [header.width] + ); + const resizableStyle: { + position: 'absolute' | 'relative'; + } = useMemo( + () => ({ + position: isDragging ? 'absolute' : 'relative', + }), + [isDragging] + ); + const resizableHandleComponent = useMemo( + () => ({ + right: , + }), + [] + ); + const handleResizeStop: ResizeCallback = useCallback( + (e, direction, ref, delta) => { + onColumnResized({ columnId: header.id, delta: delta.width }); + }, + [header.id, onColumnResized] + ); + const draggableId = useMemo( + () => + getDraggableFieldId({ + contextId: `timeline-column-headers-${timelineId}`, + fieldId: header.id, + }), + [timelineId, header.id] + ); return ( , - }} + enable={RESIZABLE_ENABLE} + size={resizableSize} + style={resizableStyle} + handleComponent={resizableHandleComponent} onResizeStop={handleResizeStop} > - {(dragProvided, dragSnapshot) => ( + {dragProvided => ( - {!dragSnapshot.isDragging ? ( - -
- - ) : ( - - - - - - )} + +
+ )} @@ -104,4 +118,16 @@ const ColumnHeaderComponent: React.FC = ({ ); }; -export const ColumnHeader = React.memo(ColumnHeaderComponent); +export const ColumnHeader = React.memo( + ColumnHeaderComponent, + (prevProps, nextProps) => + prevProps.draggableIndex === nextProps.draggableIndex && + prevProps.timelineId === nextProps.timelineId && + prevProps.isDragging === nextProps.isDragging && + prevProps.onColumnRemoved === nextProps.onColumnRemoved && + prevProps.onColumnResized === nextProps.onColumnResized && + prevProps.onColumnSorted === nextProps.onColumnSorted && + prevProps.onFilterChange === nextProps.onFilterChange && + prevProps.sort === nextProps.sort && + deepEqual(prevProps.header, nextProps.header) +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx index ab8dc629dd577..7a072f1dbf578 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx @@ -6,9 +6,12 @@ import { EuiCheckbox } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React from 'react'; -import { Droppable } from 'react-beautiful-dnd'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; +import deepEqual from 'fast-deep-equal'; +import { DragEffects } from '../../../drag_and_drop/draggable_wrapper'; +import { DraggableFieldBadge } from '../../../draggables/field_badge'; import { BrowserFields } from '../../../../containers/source'; import { ColumnHeaderOptions } from '../../../../store/timeline/model'; import { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix } from '../../../drag_and_drop/helpers'; @@ -53,6 +56,26 @@ interface Props { toggleColumn: (column: ColumnHeaderOptions) => void; } +interface DraggableContainerProps { + children: React.ReactNode; + onMount: () => void; + onUnmount: () => void; +} + +export const DraggableContainer = React.memo( + ({ children, onMount, onUnmount }) => { + useEffect(() => { + onMount(); + + return () => onUnmount(); + }, [onMount, onUnmount]); + + return <>{children}; + } +); + +DraggableContainer.displayName = 'DraggableContainer'; + /** Renders the timeline header columns */ export const ColumnHeadersComponent = ({ actionsColumnWidth, @@ -71,86 +94,157 @@ export const ColumnHeadersComponent = ({ sort, timelineId, toggleColumn, -}: Props) => ( - - - - {showEventsSelect && ( - - - - - - )} - {showSelectAllCheckbox && ( +}: Props) => { + const [draggingIndex, setDraggingIndex] = useState(null); + + const handleSelectAllChange = useCallback( + (event: React.ChangeEvent) => { + onSelectAll({ isSelected: event.currentTarget.checked }); + }, + [onSelectAll] + ); + + const renderClone: DraggableChildrenFn = useCallback( + (dragProvided, dragSnapshot, rubric) => { + // TODO: Remove after github.com/DefinitelyTyped/DefinitelyTyped/pull/43057 is merged + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const index = (rubric as any).source.index; + const header = columnHeaders[index]; + + const onMount = () => setDraggingIndex(index); + const onUnmount = () => setDraggingIndex(null); + + return ( + + + + + + + + ); + }, + [columnHeaders, setDraggingIndex] + ); + + const ColumnHeaderList = useMemo( + () => + columnHeaders.map((header, draggableIndex) => ( + + )), + [ + columnHeaders, + timelineId, + draggingIndex, + onColumnRemoved, + onFilterChange, + onColumnResized, + sort, + ] + ); + + return ( + + + + {showEventsSelect && ( + + + + + + )} + {showSelectAllCheckbox && ( + + + + + + )} - - ) => { - onSelectAll({ isSelected: event.currentTarget.checked }); - }} + + - )} - - - - - - - - - {(dropProvided, snapshot) => ( - <> - - {columnHeaders.map((header, draggableIndex) => ( - - ))} - - {dropProvided.placeholder} - - )} - - - -); + -export const ColumnHeaders = React.memo(ColumnHeadersComponent); + + {(dropProvided, snapshot) => ( + <> + + {ColumnHeaderList} + + + )} + + + + ); +}; + +export const ColumnHeaders = React.memo( + ColumnHeadersComponent, + (prevProps, nextProps) => + prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.onColumnRemoved === nextProps.onColumnRemoved && + prevProps.onColumnResized === nextProps.onColumnResized && + prevProps.onColumnSorted === nextProps.onColumnSorted && + prevProps.onSelectAll === nextProps.onSelectAll && + prevProps.onUpdateColumns === nextProps.onUpdateColumns && + prevProps.onFilterChange === nextProps.onFilterChange && + prevProps.showEventsSelect === nextProps.showEventsSelect && + prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && + prevProps.sort === nextProps.sort && + prevProps.timelineId === nextProps.timelineId && + prevProps.toggleColumn === nextProps.toggleColumn && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.browserFields, nextProps.browserFields) +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap index 93e12a0ed4fcd..75623252181db 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -6,11 +6,7 @@ exports[`Columns it renders the expected columns 1`] = ` > ( - ({ _id, columnHeaders, columnRenderers, data, ecsData, timelineId }) => { - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - {columnHeaders.map((header, index) => ( - - - {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - columnName: header.id, - eventId: _id, - field: header, - linkValues: getOr([], header.linkField ?? '', ecsData), - timelineId, - truncate: true, - values: getMappedNonEcsValue({ - data, - fieldName: header.id, - }), - })} - - - ))} - - ); - } + ({ _id, columnHeaders, columnRenderers, data, ecsData, timelineId }) => ( + + {columnHeaders.map(header => ( + + + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + columnName: header.id, + eventId: _id, + field: header, + linkValues: getOr([], header.linkField ?? '', ecsData), + timelineId, + truncate: true, + values: getMappedNonEcsValue({ + data, + fieldName: header.id, + }), + })} + + + ))} + + ) ); DataDrivenColumns.displayName = 'DataDrivenColumns'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx index 84c4253076dc9..4178bc656f32d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx @@ -51,9 +51,6 @@ interface Props { updateNote: UpdateNote; } -// Passing the styles directly to the component because the width is -// being calculated and is recommended by Styled Components for performance -// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 const EventsComponent: React.FC = ({ actionsColumnWidth, addNoteToEvent, @@ -93,7 +90,7 @@ const EventsComponent: React.FC = ({ getNotesByIds={getNotesByIds} isEventPinned={eventIsPinned({ eventId: event._id, pinnedEventIds })} isEventViewer={isEventViewer} - key={event._id} + key={`${event._id}_${event._index}`} loadingEventIds={loadingEventIds} maxDelay={maxDelay(i)} onColumnResized={onColumnResized} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx index 1f09ae4337c42..6e5c292064dc6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx @@ -25,13 +25,14 @@ import { } from '../../events'; import { ExpandableEvent } from '../../expandable_event'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; -import { EventsTrGroup, EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles'; -import { useTimelineWidthContext } from '../../timeline_context'; +import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; import { getRowRenderer } from '../renderers/get_row_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { getEventType } from '../helpers'; -import { StatefulEventChild } from './stateful_event_child'; +import { NoteCards } from '../../../notes/note_cards'; +import { useEventDetailsWidthContext } from '../../../events_viewer/event_details_width_context'; +import { EventColumnView } from './event_column_view'; interface Props { actionsColumnWidth: number; @@ -89,28 +90,14 @@ const TOP_OFFSET = 50; */ const BOTTOM_OFFSET = -500; -interface AttributesProps { - children: React.ReactNode; -} - -const AttributesComponent: React.FC = ({ children }) => { - const width = useTimelineWidthContext(); +const emptyNotes: string[] = []; - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - {children} - - ); -}; +const EventsTrSupplementContainerWrapper = React.memo(({ children }) => { + const width = useEventDetailsWidthContext(); + return {children}; +}); -const Attributes = React.memo(AttributesComponent); +EventsTrSupplementContainerWrapper.displayName = 'EventsTrSupplementContainerWrapper'; const StatefulEventComponent: React.FC = ({ actionsColumnWidth, @@ -221,60 +208,75 @@ const StatefulEventComponent: React.FC = ({ data-test-subj="event" eventType={getEventType(event.ecs)} showLeftBorder={!isEventViewer} - ref={c => { - if (c != null) { - divElement.current = c; - } - }} + ref={divElement} > - {getRowRenderer(event.ecs, rowRenderers).renderRow({ - browserFields, - data: event.ecs, - children: ( - + + + + - ), - timelineId, - })} + - - - + {getRowRenderer(event.ecs, rowRenderers).renderRow({ + browserFields, + data: event.ecs, + timelineId, + })} + + + + + )} @@ -286,10 +288,7 @@ const StatefulEventComponent: React.FC = ({ ? `${divElement.current.clientHeight}px` : DEFAULT_ROW_HEIGHT; - // height is being inlined directly in here because of performance with StyledComponents - // involving quick and constant changes to the DOM. - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ; + return ; } }} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx deleted file mode 100644 index 04f4ddf2a6eab..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import uuid from 'uuid'; - -import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; -import { Note } from '../../../../lib/note'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { NoteCards } from '../../../notes/note_cards'; -import { OnPinEvent, OnColumnResized, OnUnPinEvent, OnRowSelected } from '../../events'; -import { EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles'; -import { useTimelineWidthContext } from '../../timeline_context'; -import { ColumnRenderer } from '../renderers/column_renderer'; -import { EventColumnView } from './event_column_view'; - -interface Props { - id: string; - actionsColumnWidth: number; - addNoteToEvent: AddNoteToEvent; - onPinEvent: OnPinEvent; - columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; - data: TimelineNonEcsData[]; - ecsData: Ecs; - expanded: boolean; - eventIdToNoteIds: Readonly>; - isEventViewer?: boolean; - isEventPinned: boolean; - loading: boolean; - loadingEventIds: Readonly; - onColumnResized: OnColumnResized; - onRowSelected: OnRowSelected; - onUnPinEvent: OnUnPinEvent; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - showNotes: boolean; - timelineId: string; - updateNote: UpdateNote; - onToggleExpanded: () => void; - onToggleShowNotes: () => void; - getNotesByIds: (noteIds: string[]) => Note[]; - associateNote: (noteId: string) => void; -} - -export const getNewNoteId = (): string => uuid.v4(); - -const emptyNotes: string[] = []; - -export const StatefulEventChild = React.memo( - ({ - id, - actionsColumnWidth, - associateNote, - addNoteToEvent, - onPinEvent, - columnHeaders, - columnRenderers, - expanded, - data, - ecsData, - eventIdToNoteIds, - getNotesByIds, - isEventViewer = false, - isEventPinned = false, - loading, - loadingEventIds, - onColumnResized, - onRowSelected, - onToggleExpanded, - onUnPinEvent, - selectedEventIds, - showCheckboxes, - showNotes, - timelineId, - onToggleShowNotes, - updateNote, - }) => { - const width = useTimelineWidthContext(); - - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - <> - - - - - - - ); - } -); -StatefulEventChild.displayName = 'StatefulEventChild'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx index ea80d3351408a..fac8cc61cddd2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx @@ -38,7 +38,7 @@ export interface BodyProps { columnRenderers: ColumnRenderer[]; data: TimelineItem[]; getNotesByIds: (noteIds: string[]) => Note[]; - height: number; + height?: number; id: string; isEventViewer?: boolean; isSelectAllChecked: boolean; @@ -96,9 +96,10 @@ export const Body = React.memo( }) => { const containerElementRef = useRef(null); const timelineTypeContext = useTimelineTypeContext(); - const additionalActionWidth = - timelineTypeContext.timelineActions?.reduce((acc, v) => acc + v.width, 0) ?? 0; - + const additionalActionWidth = useMemo( + () => timelineTypeContext.timelineActions?.reduce((acc, v) => acc + v.width, 0) ?? 0, + [timelineTypeContext.timelineActions] + ); const actionsColumnWidth = useMemo( () => getActionsColumnWidth(isEventViewer, showCheckboxes, additionalActionWidth), [isEventViewer, showCheckboxes, additionalActionWidth] @@ -113,11 +114,7 @@ export const Body = React.memo( return ( <> - + - - some child - - -`; +exports[`get_column_renderer renders correctly against snapshot 1`] = ``; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap index 5731921907fc8..66a1b293cf8b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap @@ -1,9 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`plain_row_renderer renders correctly against snapshot 1`] = ` - - - some children - - -`; +exports[`plain_row_renderer renders correctly against snapshot 1`] = ``; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap index 0b2a1b2f2a0ae..b24a90589ce65 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly against snapshot 1`] = ` - - some children - - - some children - { const children = connectedToRenderer.renderRow({ browserFields, data: auditd, - children: {'some children'}, timelineId: 'test', }); @@ -66,26 +65,10 @@ describe('GenericRowRenderer', () => { } }); - test('should render children normally if it does not have a auditd object', () => { - const children = connectedToRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonAuditd, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a auditd row', () => { const children = connectedToRenderer.renderRow({ browserFields: mockBrowserFields, data: auditd, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -94,7 +77,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' + 'Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' ); }); }); @@ -119,7 +102,6 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields, data: auditdFile, - children: {'some children'}, timelineId: 'test', }); @@ -145,26 +127,10 @@ describe('GenericRowRenderer', () => { } }); - test('should render children normally if it does not have a auditd object', () => { - const children = fileToRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonAuditd, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a auditd row', () => { const children = fileToRenderer.renderRow({ browserFields: mockBrowserFields, data: auditdFile, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -173,7 +139,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' + 'Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx index bcf464ab6da15..4ed4ae10ed810 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx @@ -32,19 +32,16 @@ export const createGenericAuditRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -67,20 +64,17 @@ export const createGenericFileRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx index f367769b78f40..7ad8cfed5256b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -38,7 +38,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, - children: {'some child'}, timelineId: 'test', }); @@ -51,7 +50,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, - children: {'some child'}, timelineId: 'test', }); const wrapper = mount( @@ -59,7 +57,7 @@ describe('get_column_renderer', () => { {row} ); - expect(wrapper.text()).toContain('some child'); + expect(wrapper.text()).toEqual(''); }); test('should render a suricata row data when it is a suricata row', () => { @@ -67,7 +65,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -76,7 +73,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' + '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -86,7 +83,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -95,7 +91,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' + '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -105,7 +101,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: zeek, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -114,7 +109,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' + 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); @@ -124,7 +119,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: system, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -133,7 +127,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child Braden@zeek-londonattempted a login via(6278)with resultfailureSource128.199.212.120' + 'Braden@zeek-londonattempted a login via(6278)with resultfailureSource128.199.212.120' ); }); @@ -143,7 +137,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: auditd, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -152,7 +145,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child Sessionalice@zeek-sanfranin/executedgpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' + 'Sessionalice@zeek-sanfranin/executedgpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap index 4326b7372604d..d7bdacbcc61ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`netflowRowRenderer renders correctly against snapshot 1`] = ` - - some children -
{ const children = netflowRowRenderer.renderRow({ browserFields, data: getMockNetflowData(), - children: {'some children'}, timelineId: 'test', }); @@ -98,26 +97,10 @@ describe('netflowRowRenderer', () => { }); }); - test('should render children normally when given non-netflow data', () => { - const children = netflowRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: justIdAndTimestamp, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render netflow data', () => { const children = netflowRowRenderer.renderRow({ browserFields: mockBrowserFields, data: getMockNetflowData(), - children: {'some children'}, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx index 754d6ad99b7fe..10d80e1952f40 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx @@ -78,73 +78,63 @@ export const eventActionMatches = (eventAction: string | object | undefined | nu }; export const netflowRowRenderer: RowRenderer = { - isInstance: ecs => { - return ( - eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || - eventActionMatches(get(EVENT_ACTION_FIELD, ecs)) - ); - }, - renderRow: ({ data, children, timelineId }) => ( - <> - {children} - -
- -
-
- + isInstance: ecs => + eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || + eventActionMatches(get(EVENT_ACTION_FIELD, ecs)), + renderRow: ({ data, timelineId }) => ( + +
+ +
+
), }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx index 50ea7ca05b921..467f507e8be7d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx @@ -25,7 +25,6 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, - children: {'some children'}, timelineId: 'test', }); const wrapper = shallow({children}); @@ -40,7 +39,6 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, - children: {'some children'}, timelineId: 'test', }); const wrapper = mount( @@ -48,6 +46,6 @@ describe('plain_row_renderer', () => { {children} ); - expect(wrapper.text()).toEqual('some children'); + expect(wrapper.text()).toEqual(''); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx index 6725830c97d0a..da78f41f09ed4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx @@ -12,5 +12,5 @@ import { RowRenderer } from './row_renderer'; export const plainRowRenderer: RowRenderer = { isInstance: _ => true, - renderRow: ({ children }) => <>{children}, + renderRow: () => <>, }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx index df92fc1e9f634..2d9f877fe4af0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx @@ -8,28 +8,17 @@ import React from 'react'; import { BrowserFields } from '../../../../containers/source'; import { Ecs } from '../../../../graphql/types'; -import { EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles'; -import { useTimelineWidthContext } from '../../timeline_context'; +import { EventsTrSupplement } from '../../styles'; interface RowRendererContainerProps { children: React.ReactNode; } -export const RowRendererContainer = React.memo(({ children }) => { - const width = useTimelineWidthContext(); - - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - {children} - - ); -}); +export const RowRendererContainer = React.memo(({ children }) => ( + + {children} + +)); RowRendererContainer.displayName = 'RowRendererContainer'; export interface RowRenderer { @@ -37,12 +26,10 @@ export interface RowRenderer { renderRow: ({ browserFields, data, - children, timelineId, }: { browserFields: BrowserFields; data: Ecs; - children: React.ReactNode; timelineId: string; }) => React.ReactNode; } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap index 3608a81234677..93b3046b57ed6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = ` - - some children - { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, - children: {'some children'}, timelineId: 'test', }); @@ -45,26 +44,10 @@ describe('suricata_row_renderer', () => { expect(suricataRowRenderer.isInstance(suricata)).toBe(true); }); - test('should render children normally if it does not have a signature', () => { - const children = suricataRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonSuricata, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a suricata row', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -73,7 +56,7 @@ describe('suricata_row_renderer', () => { ); expect(wrapper.text()).toContain( - 'some children 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' + '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -82,7 +65,6 @@ describe('suricata_row_renderer', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some children'}, timelineId: 'test', }); const wrapper = mount( @@ -90,6 +72,6 @@ describe('suricata_row_renderer', () => { {children} ); - expect(wrapper.text()).toEqual('some children'); + expect(wrapper.text()).toEqual(''); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx index b227326551e01..e49a5f65b47c1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx @@ -17,15 +17,9 @@ export const suricataRowRenderer: RowRenderer = { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'suricata'; }, - renderRow: ({ browserFields, data, children, timelineId }) => { - return ( - <> - {children} - - - - - - ); - }, + renderRow: ({ browserFields, data, timelineId }) => ( + + + + ), }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index 2b9adfe21b120..9ccd1fb7a0519 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; @@ -28,11 +28,9 @@ const SignatureFlexItem = styled(EuiFlexItem)` SignatureFlexItem.displayName = 'SignatureFlexItem'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -const Badge = (props: EuiBadgeProps) => ; +const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap index 9ed6587145584..6fff32925abf3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly against snapshot 1`] = ` - - some children - - - some children - { const children = connectedToRenderer.renderRow({ browserFields, data: system, - children: {'some children'}, timelineId: 'test', }); @@ -99,7 +98,6 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields: mockBrowserFields, data: system, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -108,7 +106,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Evan@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' + 'Evan@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' ); }); }); @@ -133,7 +131,6 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields, data: systemFile, - children: {'some children'}, timelineId: 'test', }); @@ -162,7 +159,6 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields: mockBrowserFields, data: systemFile, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -171,7 +167,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Braden@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' + 'Braden@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' ); }); }); @@ -195,14 +191,13 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54started processMicrosoft.Photos.exe(441684)C:\\Program Files\\WindowsApps\\Microsoft.Windows.Photos_2018.18091.17210.0_x64__8wekyb3d8bbwe\\Microsoft.Photos.exe-ServerName:App.AppXzst44mncqdg84v7sv6p7yznqwssy6f7f.mcavia parent processsvchost.exe(8)d4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee12563599116157778a22600d2a163d8112aed84562d06d7235b37895b68de56687895743' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54started processMicrosoft.Photos.exe(441684)C:\\Program Files\\WindowsApps\\Microsoft.Windows.Photos_2018.18091.17210.0_x64__8wekyb3d8bbwe\\Microsoft.Photos.exe-ServerName:App.AppXzst44mncqdg84v7sv6p7yznqwssy6f7f.mcavia parent processsvchost.exe(8)d4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee12563599116157778a22600d2a163d8112aed84562d06d7235b37895b68de56687895743' ); }); @@ -224,14 +219,13 @@ describe('GenericRowRenderer', () => { endgameProcessTerminationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameTerminationEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54terminated processRuntimeBroker.exe(442384)with exit code087976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776797255e72d5ed5c058d4785950eba7abaa057653bd4401441a21bf1abce6404f4231db4d' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54terminated processRuntimeBroker.exe(442384)with exit code087976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776797255e72d5ed5c058d4785950eba7abaa057653bd4401441a21bf1abce6404f4231db4d' ); }); @@ -253,7 +247,6 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} @@ -284,7 +277,6 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} @@ -315,7 +307,6 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} @@ -344,14 +335,13 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54created a fileinC:\\Users\\Arun\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\63d78c21-e593-4484-b7a9-db33cd522ddc.tmpviachrome.exe(11620)' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54created a fileinC:\\Users\\Arun\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\63d78c21-e593-4484-b7a9-db33cd522ddc.tmpviachrome.exe(11620)' ); }); @@ -373,14 +363,13 @@ describe('GenericRowRenderer', () => { endgameFileDeleteEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileDeleteEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-v1s-d2118419deleted a filetmp000002f6inC:\\Windows\\TEMP\\tmp00000404\\tmp000002f6viaAmSvc.exe(1084)' + 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419deleted a filetmp000002f6inC:\\Windows\\TEMP\\tmp00000404\\tmp000002f6viaAmSvc.exe(1084)' ); }); @@ -402,15 +391,12 @@ describe('GenericRowRenderer', () => { fileCreatedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileCreatedEvent, - children: {'some children '}, timelineId: 'test', })} ); - expect(wrapper.text()).toEqual( - 'some children foohostcreated a filein/etc/subgidviaan unknown process' - ); + expect(wrapper.text()).toEqual('foohostcreated a filein/etc/subgidviaan unknown process'); }); test('it renders a FIM (non-endgame) file deleted event', () => { @@ -431,14 +417,13 @@ describe('GenericRowRenderer', () => { fileDeletedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileDeletedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children foohostdeleted a filein/etc/gshadow.lockviaan unknown process' + 'foohostdeleted a filein/etc/gshadow.lockviaan unknown process' ); }); @@ -460,7 +445,6 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, - children: {'some children '}, timelineId: 'test', })} @@ -491,7 +475,6 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, - children: {'some children '}, timelineId: 'test', })} @@ -522,7 +505,6 @@ describe('GenericRowRenderer', () => { fileCreatedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileCreatedEvent, - children: {'some children '}, timelineId: 'test', })} @@ -551,14 +533,13 @@ describe('GenericRowRenderer', () => { endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4ConnectionAcceptEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' + 'SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' ); }); @@ -580,14 +561,13 @@ describe('GenericRowRenderer', () => { endgameIpv6ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv6ConnectionAcceptEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' + 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' ); }); @@ -609,14 +589,13 @@ describe('GenericRowRenderer', () => { endgameIpv4DisconnectReceivedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4DisconnectReceivedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' ); }); @@ -638,14 +617,13 @@ describe('GenericRowRenderer', () => { endgameIpv6DisconnectReceivedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv6DisconnectReceivedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' + 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' ); }); @@ -667,14 +645,13 @@ describe('GenericRowRenderer', () => { socketOpenedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: socketOpenedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' + 'root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' ); }); @@ -696,14 +673,13 @@ describe('GenericRowRenderer', () => { socketClosedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: socketClosedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' + 'root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' ); }); @@ -725,7 +701,6 @@ describe('GenericRowRenderer', () => { endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4ConnectionAcceptEvent, - children: {'some children '}, timelineId: 'test', })} @@ -750,14 +725,13 @@ describe('GenericRowRenderer', () => { userLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogonEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-v1s-d2118419successfully logged inusing logon type5 - Service(target logon ID0x3e7)viaC:\\Windows\\System32\\services.exe(432)as requested by subjectWIN-Q3DOP1UKA81$(subject logon ID0x3e7)4624' + 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419successfully logged inusing logon type5 - Service(target logon ID0x3e7)viaC:\\Windows\\System32\\services.exe(432)as requested by subjectWIN-Q3DOP1UKA81$(subject logon ID0x3e7)4624' ); }); @@ -775,14 +749,13 @@ describe('GenericRowRenderer', () => { adminLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: adminLogonEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children With special privileges,SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54successfully logged inviaC:\\Windows\\System32\\lsass.exe(964)as requested by subjectSYSTEM\\NT AUTHORITY4672' + 'With special privileges,SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54successfully logged inviaC:\\Windows\\System32\\lsass.exe(964)as requested by subjectSYSTEM\\NT AUTHORITY4672' ); }); @@ -800,14 +773,13 @@ describe('GenericRowRenderer', () => { explicitUserLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: explicitUserLogonEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children A login was attempted using explicit credentialsArun\\Anvi-AcertoHD-55b-3ec87f66viaC:\\Windows\\System32\\svchost.exe(1736)as requested by subjectANVI-ACER$\\WORKGROUP(subject logon ID0x3e7)4648' + 'A login was attempted using explicit credentialsArun\\Anvi-AcertoHD-55b-3ec87f66viaC:\\Windows\\System32\\svchost.exe(1736)as requested by subjectANVI-ACER$\\WORKGROUP(subject logon ID0x3e7)4648' ); }); @@ -825,14 +797,13 @@ describe('GenericRowRenderer', () => { userLogoffEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogoffEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-55b-3ec87f66logged offusing logon type2 - Interactive(target logon ID0x16db41e)viaC:\\Windows\\System32\\lsass.exe(964)4634' + 'Arun\\Anvi-Acer@HD-55b-3ec87f66logged offusing logon type2 - Interactive(target logon ID0x16db41e)viaC:\\Windows\\System32\\lsass.exe(964)4634' ); }); @@ -850,7 +821,6 @@ describe('GenericRowRenderer', () => { userLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogonEvent, - children: {'some children '}, timelineId: 'test', })} @@ -874,14 +844,13 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54asked forupdate.googleapis.comwith question typeA, which resolved to10.100.197.67viaGoogleUpdate.exe(443192)3008dns' + 'SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54asked forupdate.googleapis.comwith question typeA, which resolved to10.100.197.67viaGoogleUpdate.exe(443192)3008dns' ); }); @@ -898,14 +867,13 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: dnsEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' + 'iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' ); }); @@ -928,7 +896,6 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, - children: {'some children '}, timelineId: 'test', })} @@ -956,7 +923,6 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, - children: {'some children '}, timelineId: 'test', })} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx index 3e64248d39876..523d4f3a0cfb8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -35,19 +35,16 @@ export const createGenericSystemRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -68,20 +65,17 @@ export const createEndgameProcessRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -102,20 +96,17 @@ export const createFimRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -136,19 +127,16 @@ export const createGenericFileRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -163,19 +151,16 @@ export const createSocketRowRenderer = ({ const action: string | null | undefined = get('event.action[0]', ecs); return action != null && action.toLowerCase() === actionName; }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -194,18 +179,15 @@ export const createSecurityEventRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -215,18 +197,15 @@ export const createDnsRowRenderer = (): RowRenderer => ({ const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs); return !isNillEmptyOrNotFinite(dnsQuestionType) && !isNillEmptyOrNotFinite(dnsQuestionName); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap index 9b59f69cad3a3..460ad35b47678 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = ` - - some children - { const children = zeekRowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonZeek, - children: {'some children'}, timelineId: 'test', }); @@ -44,26 +43,10 @@ describe('zeek_row_renderer', () => { expect(zeekRowRenderer.isInstance(zeek)).toBe(true); }); - test('should render children normally if it does not have a zeek object', () => { - const children = zeekRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonZeek, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a zeek row', () => { const children = zeekRowRenderer.renderRow({ browserFields: mockBrowserFields, data: zeek, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -72,7 +55,7 @@ describe('zeek_row_renderer', () => { ); expect(wrapper.text()).toContain( - 'some children C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' + 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx index fc528e33b5ab6..0fca5cdd8b3d4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx @@ -17,12 +17,9 @@ export const zeekRowRenderer: RowRenderer = { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'zeek'; }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index 57e5ff19eb815..f13a236e8ec36 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { get } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; @@ -19,11 +19,9 @@ import { IS_OPERATOR } from '../../../data_providers/data_provider'; import * as i18n from './translations'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -const Badge = (props: EuiBadgeProps) => ; +const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx index d06dcbb84ad78..76f26d3dda5af 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx @@ -8,6 +8,7 @@ import { noop } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import React, { useCallback, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { BrowserFields } from '../../../containers/source'; import { TimelineItem } from '../../../graphql/types'; @@ -38,9 +39,9 @@ import { plainRowRenderer } from './renderers/plain_row_renderer'; interface OwnProps { browserFields: BrowserFields; data: TimelineItem[]; + height?: number; id: string; isEventViewer?: boolean; - height: number; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; } @@ -101,7 +102,7 @@ const StatefulBodyComponent = React.memo( isSelected && Object.keys(selectedEventIds).length + 1 === data.length, }); }, - [id, data, selectedEventIds, timelineTypeContext.queryFields] + [setSelected, id, data, selectedEventIds, timelineTypeContext.queryFields] ); const onSelectAll: OnSelectAll = useCallback( @@ -118,7 +119,7 @@ const StatefulBodyComponent = React.memo( isSelectAllChecked: isSelected, }) : clearSelected!({ id }), - [id, data, timelineTypeContext.queryFields] + [setSelected, clearSelected, id, data, timelineTypeContext.queryFields] ); const onColumnSorted: OnColumnSorted = useCallback( @@ -189,25 +190,22 @@ const StatefulBodyComponent = React.memo( /> ); }, - (prevProps, nextProps) => { - return ( - prevProps.browserFields === nextProps.browserFields && - prevProps.columnHeaders === nextProps.columnHeaders && - prevProps.data === nextProps.data && - prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && - prevProps.notesById === nextProps.notesById && - prevProps.height === nextProps.height && - prevProps.id === nextProps.id && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.pinnedEventIds === nextProps.pinnedEventIds && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && - prevProps.sort === nextProps.sort - ); - } + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.data, nextProps.data) && + prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && + deepEqual(prevProps.notesById, nextProps.notesById) && + prevProps.height === nextProps.height && + prevProps.id === nextProps.id && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.loadingEventIds === nextProps.loadingEventIds && + prevProps.pinnedEventIds === nextProps.pinnedEventIds && + prevProps.selectedEventIds === nextProps.selectedEventIds && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.showRowRenderers === nextProps.showRowRenderers && + prevProps.sort === nextProps.sort ); StatefulBodyComponent.displayName = 'StatefulBodyComponent'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index a47fb932ed26c..56639f90c1464 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiText } from '@elastic/eui'; +import { EuiBadge, EuiText } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; @@ -21,24 +21,12 @@ const Text = styled(EuiText)` Text.displayName = 'Text'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const BadgeHighlighted = styled(EuiBadge)` -// height: 20px; -// margin: 0 5px 0 5px; -// max-width: 70px; -// min-width: 70px; -// `; -const BadgeHighlighted = (props: EuiBadgeProps) => ( - -); +const BadgeHighlighted = styled(EuiBadge)` + height: 20px; + margin: 0 5px 0 5px; + maxwidth: 85px; + minwidth: 85px; +`; BadgeHighlighted.displayName = 'BadgeHighlighted'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx index 1a1e8292b7e02..663b3dd501341 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { rgba } from 'polished'; -import React from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; import { AndOrBadge } from '../../and_or_badge'; @@ -54,13 +54,9 @@ const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>` DropAndTargetDataProviders.displayName = 'DropAndTargetDataProviders'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const NumberProviderAndBadge = styled(EuiBadge)` -// margin: 0px 5px; -// `; -const NumberProviderAndBadge = (props: EuiBadgeProps) => ( - -); +const NumberProviderAndBadge = styled(EuiBadge)` + margin: 0px 5px; +`; NumberProviderAndBadge.displayName = 'NumberProviderAndBadge'; @@ -89,8 +85,13 @@ export const ProviderItemAndDragDrop = React.memo( onToggleDataProviderExcluded, timelineId, }) => { - const onMouseEnter = () => onChangeDroppableAndProvider(dataProvider.id); - const onMouseLeave = () => onChangeDroppableAndProvider(''); + const onMouseEnter = useCallback(() => onChangeDroppableAndProvider(dataProvider.id), [ + onChangeDroppableAndProvider, + dataProvider.id, + ]); + const onMouseLeave = useCallback(() => onChangeDroppableAndProvider(''), [ + onChangeDroppableAndProvider, + ]); const hasAndItem = dataProvider.and.length > 0; return ( ` ${({ hideExpandButton }) => @@ -50,33 +49,26 @@ export const ExpandableEvent = React.memo( timelineId, toggleColumn, onUpdateColumns, - }) => { - const width = useTimelineWidthContext(); - // Passing the styles directly to the component of LazyAccordion because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - ( - - )} - forceExpand={forceExpand} - paddingSize="none" - /> - - ); - } + }) => ( + + ( + + )} + forceExpand={forceExpand} + paddingSize="none" + /> + + ) ); ExpandableEvent.displayName = 'ExpandableEvent'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx index 65c539d77a16b..16eaa80308205 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx @@ -6,6 +6,7 @@ import { memo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from 'src/plugins/data/public'; import { timelineSelectors, State } from '../../store'; @@ -39,7 +40,14 @@ const TimelineKqlFetchComponent = memo( }); }, [kueryFilterQueryDraft, kueryFilterQuery, id]); return null; - } + }, + (prevProps, nextProps) => + prevProps.id === nextProps.id && + prevProps.inputId === nextProps.inputId && + prevProps.setTimelineQuery === nextProps.setTimelineQuery && + deepEqual(prevProps.kueryFilterQuery, nextProps.kueryFilterQuery) && + deepEqual(prevProps.kueryFilterQueryDraft, nextProps.kueryFilterQueryDraft) && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) ); const makeMapStateToProps = () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap index 9cdbda757d97e..eff487ceb7981 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap @@ -1,94 +1,86 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Footer Timeline Component rendering it renders the default timeline footer 1`] = ` - - + - - - - - 1 rows - , - - 5 rows - , - - 10 rows - , - - 20 rows - , - ] - } - itemsCount={2} - onClick={[Function]} - serverSideEventCount={15546} - /> - - - - + 1 rows + , + + 5 rows + , + + 10 rows + , + + 20 rows + , + ] + } + itemsCount={2} + onClick={[Function]} + serverSideEventCount={15546} /> - - - - - - - - - + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx index cbad2d42cf8af..d54a4cee83e52 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx @@ -17,7 +17,6 @@ describe('Footer Timeline Component', () => { const loadMore = jest.fn(); const onChangeItemsPerPage = jest.fn(); const getUpdatedAt = () => 1546878704036; - const compact = true; describe('rendering', () => { test('it renders the default timeline footer', () => { @@ -36,7 +35,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -59,7 +57,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -83,7 +80,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -140,7 +136,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -164,7 +159,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -195,7 +189,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -225,7 +218,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -259,7 +251,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -285,7 +276,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx index 1fcc4382c1798..7a025e96e57f2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx @@ -19,7 +19,7 @@ import { EuiPopoverProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useState, useMemo } from 'react'; import styled from 'styled-components'; import { LoadingPanel } from '../../loading'; @@ -28,8 +28,30 @@ import { OnChangeItemsPerPage, OnLoadMore } from '../events'; import { LastUpdatedAt } from './last_updated'; import * as i18n from './translations'; import { useTimelineTypeContext } from '../timeline_context'; +import { useEventDetailsWidthContext } from '../../events_viewer/event_details_width_context'; -const FixedWidthLastUpdated = styled.div<{ compact: boolean }>` +export const isCompactFooter = (width: number): boolean => width < 600; + +interface FixedWidthLastUpdatedContainerProps { + updatedAt: number; +} + +const FixedWidthLastUpdatedContainer = React.memo( + ({ updatedAt }) => { + const width = useEventDetailsWidthContext(); + const compact = useMemo(() => isCompactFooter(width), [width]); + + return ( + + + + ); + } +); + +FixedWidthLastUpdatedContainer.displayName = 'FixedWidthLastUpdatedContainer'; + +const FixedWidthLastUpdated = styled.div<{ compact?: boolean }>` width: ${({ compact }) => (!compact ? 200 : 25)}px; overflow: hidden; text-align: end; @@ -37,8 +59,16 @@ const FixedWidthLastUpdated = styled.div<{ compact: boolean }>` FixedWidthLastUpdated.displayName = 'FixedWidthLastUpdated'; -const FooterContainer = styled(EuiFlexGroup)<{ height: number }>` - height: ${({ height }) => height}px; +interface HeightProp { + height: number; +} + +const FooterContainer = styled(EuiFlexGroup).attrs(({ height }) => ({ + style: { + height: `${height}px`, + }, +}))` + flex: 0; `; FooterContainer.displayName = 'FooterContainer'; @@ -56,7 +86,7 @@ const LoadingPanelContainer = styled.div` LoadingPanelContainer.displayName = 'LoadingPanelContainer'; -const PopoverRowItems = styled((EuiPopover as unknown) as FunctionComponent)< +const PopoverRowItems = styled((EuiPopover as unknown) as FC)< EuiPopoverProps & { className?: string; id?: string; @@ -173,11 +203,9 @@ export const PagingControl = React.memo(PagingControlComponent); PagingControl.displayName = 'PagingControl'; interface FooterProps { - compact: boolean; getUpdatedAt: () => number; hasNextPage: boolean; height: number; - isEventViewer?: boolean; isLive: boolean; isLoading: boolean; itemsCount: number; @@ -192,11 +220,9 @@ interface FooterProps { /** Renders a loading indicator and paging controls */ export const FooterComponent = ({ - compact, getUpdatedAt, hasNextPage, height, - isEventViewer, isLive, isLoading, itemsCount, @@ -216,11 +242,13 @@ export const FooterComponent = ({ const loadMore = useCallback(() => { setPaginationLoading(true); onLoadMore(nextCursor, tieBreaker); - }, [nextCursor, tieBreaker, onLoadMore]); + }, [nextCursor, tieBreaker, onLoadMore, setPaginationLoading]); - const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); - - const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [ + isPopoverOpen, + setIsPopoverOpen, + ]); + const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); useEffect(() => { if (paginationLoading && !isLoading) { @@ -263,95 +291,78 @@ export const FooterComponent = ({ )); return ( - <> - + - - - - - - - - - {isLive ? ( - - - {i18n.AUTO_REFRESH_ACTIVE}{' '} - - } - type="iInCircle" - /> - - - ) : ( - - )} - - - - - - - - - - + + + + + + + + {isLive ? ( + + + {i18n.AUTO_REFRESH_ACTIVE}{' '} + + } + type="iInCircle" + /> + + + ) : ( + + )} + + + + + + + ); }; FooterComponent.displayName = 'FooterComponent'; -export const Footer = React.memo( - FooterComponent, - (prevProps, nextProps) => - prevProps.compact === nextProps.compact && - prevProps.hasNextPage === nextProps.hasNextPage && - prevProps.height === nextProps.height && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isLive === nextProps.isLive && - prevProps.isLoading === nextProps.isLoading && - prevProps.itemsCount === nextProps.itemsCount && - prevProps.itemsPerPage === nextProps.itemsPerPage && - prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && - prevProps.serverSideEventCount === nextProps.serverSideEventCount -); +export const Footer = React.memo(FooterComponent); Footer.displayName = 'Footer'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap index 048ca080772f6..90d0dc1a8a66d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header rendering renders correctly against snapshot 1`] = ` - + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx index 5af7aff4f8795..317c68b63f691 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx @@ -7,13 +7,12 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { Direction } from '../../../graphql/types'; import { mockIndexPattern } from '../../../mock'; import { TestProviders } from '../../../mock/test_providers'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../utils/use_mount_appended'; -import { TimelineHeaderComponent } from '.'; +import { TimelineHeader } from '.'; jest.mock('../../../lib/kibana'); @@ -24,7 +23,7 @@ describe('Header', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - { onToggleDataProviderExcluded={jest.fn()} show={true} showCallOutUnauthorizedMsg={false} - sort={{ - columnId: '@timestamp', - sortDirection: Direction.desc, - }} /> ); expect(wrapper).toMatchSnapshot(); @@ -49,7 +44,7 @@ describe('Header', () => { test('it renders the data providers', () => { const wrapper = mount( - { onToggleDataProviderExcluded={jest.fn()} show={true} showCallOutUnauthorizedMsg={false} - sort={{ - columnId: '@timestamp', - sortDirection: Direction.desc, - }} /> ); @@ -76,7 +67,7 @@ describe('Header', () => { test('it renders the unauthorized call out providers', () => { const wrapper = mount( - { onToggleDataProviderExcluded={jest.fn()} show={true} showCallOutUnauthorizedMsg={true} - sort={{ - columnId: '@timestamp', - sortDirection: Direction.desc, - }} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx index 81eef0efbfa5b..7cac03cec42b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx @@ -6,10 +6,9 @@ import { EuiCallOut } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; import { IIndexPattern } from 'src/plugins/data/public'; +import deepEqual from 'fast-deep-equal'; -import { Sort } from '../body/sort'; import { DataProviders } from '../data_providers'; import { DataProvider } from '../data_providers/data_provider'; import { @@ -38,16 +37,9 @@ interface Props { onToggleDataProviderExcluded: OnToggleDataProviderExcluded; show: boolean; showCallOutUnauthorizedMsg: boolean; - sort: Sort; } -const TimelineHeaderContainer = styled.div` - width: 100%; -`; - -TimelineHeaderContainer.displayName = 'TimelineHeaderContainer'; - -export const TimelineHeaderComponent: React.FC = ({ +const TimelineHeaderComponent: React.FC = ({ browserFields, id, indexPattern, @@ -61,7 +53,7 @@ export const TimelineHeaderComponent: React.FC = ({ show, showCallOutUnauthorizedMsg, }) => ( - + <> {showCallOutUnauthorizedMsg && ( = ({ indexPattern={indexPattern} timelineId={id} /> - + ); -export const TimelineHeader = React.memo(TimelineHeaderComponent); +export const TimelineHeader = React.memo( + TimelineHeaderComponent, + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + prevProps.id === nextProps.id && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + prevProps.onChangeDataProviderKqlQuery === nextProps.onChangeDataProviderKqlQuery && + prevProps.onChangeDroppableAndProvider === nextProps.onChangeDroppableAndProvider && + prevProps.onDataProviderEdited === nextProps.onDataProviderEdited && + prevProps.onDataProviderRemoved === nextProps.onDataProviderRemoved && + prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled && + prevProps.onToggleDataProviderExcluded === nextProps.onToggleDataProviderExcluded && + prevProps.show === nextProps.show && + prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx index 611d08e61be22..f051bbe5b1af6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx @@ -153,25 +153,6 @@ export const combineQueries = ({ }; }; -interface CalculateBodyHeightParams { - /** The the height of the flyout container, which is typically the entire "page", not including the standard Kibana navigation */ - flyoutHeight?: number; - /** The flyout header typically contains a title and a close button */ - flyoutHeaderHeight?: number; - /** All non-body timeline content (i.e. the providers drag and drop area, and the column headers) */ - timelineHeaderHeight?: number; - /** Footer content that appears below the body (i.e. paging controls) */ - timelineFooterHeight?: number; -} - -export const calculateBodyHeight = ({ - flyoutHeight = 0, - flyoutHeaderHeight = 0, - timelineHeaderHeight = 0, - timelineFooterHeight = 0, -}: CalculateBodyHeightParams): number => - flyoutHeight - (flyoutHeaderHeight + timelineHeaderHeight + timelineFooterHeight); - /** * The CSS class name of a "stateful event", which appears in both * the `Timeline` and the `Events Viewer` widget diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index 0ce6bc16f1325..35099e3836fb4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -28,8 +28,8 @@ import { Timeline } from './timeline'; export interface OwnProps { id: string; - flyoutHeaderHeight: number; - flyoutHeight: number; + onClose: () => void; + usersViewing: string[]; } type Props = OwnProps & PropsFromRedux; @@ -42,14 +42,13 @@ const StatefulTimelineComponent = React.memo( eventType, end, filters, - flyoutHeaderHeight, - flyoutHeight, id, isLive, itemsPerPage, itemsPerPageOptions, kqlMode, kqlQueryExpression, + onClose, onDataProviderEdited, removeColumn, removeProvider, @@ -63,6 +62,7 @@ const StatefulTimelineComponent = React.memo( updateHighlightedDropAndProviderId, updateItemsPerPage, upsertColumn, + usersViewing, }) => { const { loading, signalIndexExists, signalIndexName } = useSignalIndex(); @@ -173,8 +173,6 @@ const StatefulTimelineComponent = React.memo( end={end} eventType={eventType} filters={filters} - flyoutHeaderHeight={flyoutHeaderHeight} - flyoutHeight={flyoutHeight} id={id} indexPattern={indexPattern} indexToAdd={indexToAdd} @@ -187,6 +185,7 @@ const StatefulTimelineComponent = React.memo( onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery} onChangeDroppableAndProvider={onChangeDroppableAndProvider} onChangeItemsPerPage={onChangeItemsPerPage} + onClose={onClose} onDataProviderEdited={onDataProviderEditedLocal} onDataProviderRemoved={onDataProviderRemoved} onToggleDataProviderEnabled={onToggleDataProviderEnabled} @@ -196,6 +195,7 @@ const StatefulTimelineComponent = React.memo( sort={sort!} start={start} toggleColumn={toggleColumn} + usersViewing={usersViewing} /> )} @@ -205,8 +205,6 @@ const StatefulTimelineComponent = React.memo( return ( prevProps.eventType === nextProps.eventType && prevProps.end === nextProps.end && - prevProps.flyoutHeaderHeight === nextProps.flyoutHeaderHeight && - prevProps.flyoutHeight === nextProps.flyoutHeight && prevProps.id === nextProps.id && prevProps.isLive === nextProps.isLive && prevProps.itemsPerPage === nextProps.itemsPerPage && @@ -219,7 +217,8 @@ const StatefulTimelineComponent = React.memo( deepEqual(prevProps.dataProviders, nextProps.dataProviders) && deepEqual(prevProps.filters, nextProps.filters) && deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && - deepEqual(prevProps.sort, nextProps.sort) + deepEqual(prevProps.sort, nextProps.sort) && + deepEqual(prevProps.usersViewing, nextProps.usersViewing) ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx index ae139c24d0176..4b1fd4b5851c0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -6,7 +6,6 @@ import { EuiBadge, - EuiBadgeProps, EuiButton, EuiButtonEmpty, EuiButtonIcon, @@ -18,8 +17,9 @@ import { EuiOverlayMask, EuiToolTip, } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; import uuid from 'uuid'; +import styled from 'styled-components'; import { Note } from '../../../lib/note'; import { Notes } from '../../notes'; @@ -32,13 +32,10 @@ export const historyToolTip = 'The chronological history of actions related to t export const streamLiveToolTip = 'Update the Timeline as new data arrives'; export const newTimelineToolTip = 'Create a new timeline'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const NotesCountBadge = styled(EuiBadge)` -// margin-left: 5px; -// `; -const NotesCountBadge = (props: EuiBadgeProps) => ( - -); +const NotesCountBadge = styled(EuiBadge)` + margin-left: 5px; +`; + NotesCountBadge.displayName = 'NotesCountBadge'; type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; @@ -121,20 +118,24 @@ interface NewTimelineProps { } export const NewTimeline = React.memo( - ({ createTimeline, onClosePopover, timelineId }) => ( - { - createTimeline({ id: timelineId, show: true }); - onClosePopover(); - }} - > - {i18n.NEW_TIMELINE} - - ) + ({ createTimeline, onClosePopover, timelineId }) => { + const handleClick = useCallback(() => { + createTimeline({ id: timelineId, show: true }); + onClosePopover(); + }, [createTimeline, timelineId, onClosePopover]); + + return ( + + {i18n.NEW_TIMELINE} + + ); + } ); NewTimeline.displayName = 'NewTimeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx index 495b94f8c02e7..e942c8f36dc83 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx @@ -10,11 +10,18 @@ import { Provider as ReduxStoreProvider } from 'react-redux'; import { mockGlobalState, apolloClientObservable } from '../../../mock'; import { createStore, State } from '../../../store'; +import { useThrottledResizeObserver } from '../../utils'; import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; jest.mock('../../../lib/kibana'); +let mockedWidth = 1000; +jest.mock('../../utils'); +(useThrottledResizeObserver as jest.Mock).mockImplementation(() => ({ + width: mockedWidth, +})); + describe('Properties', () => { const usersViewing = ['elastic']; @@ -24,6 +31,7 @@ describe('Properties', () => { beforeEach(() => { jest.clearAllMocks(); store = createStore(state, apolloClientObservable); + mockedWidth = 1000; }); test('renders correctly', () => { @@ -46,7 +54,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -73,7 +80,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -101,7 +107,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -131,7 +136,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -164,7 +168,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -197,7 +200,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -229,7 +231,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -243,7 +244,7 @@ describe('Properties', () => { test('it renders a description on the left when the width is at least as wide as the threshold', () => { const description = 'strange'; - const width = showDescriptionThreshold; + mockedWidth = showDescriptionThreshold; const wrapper = mount( @@ -264,7 +265,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -280,7 +280,7 @@ describe('Properties', () => { test('it does NOT render a description on the left when the width is less than the threshold', () => { const description = 'strange'; - const width = showDescriptionThreshold - 1; + mockedWidth = showDescriptionThreshold - 1; const wrapper = mount( @@ -301,7 +301,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -315,7 +314,7 @@ describe('Properties', () => { }); test('it renders a notes button on the left when the width is at least as wide as the threshold', () => { - const width = showNotesThreshold; + mockedWidth = showNotesThreshold; const wrapper = mount( @@ -336,7 +335,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -350,7 +348,7 @@ describe('Properties', () => { }); test('it does NOT render a a notes button on the left when the width is less than the threshold', () => { - const width = showNotesThreshold - 1; + mockedWidth = showNotesThreshold - 1; const wrapper = mount( @@ -371,7 +369,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -404,7 +401,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -434,7 +430,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -462,7 +457,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx index 7b69e006f48ad..8549784b8ecd6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; +import { useThrottledResizeObserver } from '../../utils'; import { Note } from '../../../lib/note'; import { InputsModelId } from '../../../store/inputs/constants'; import { AssociateNote, UpdateNote } from '../../notes/helpers'; @@ -37,7 +38,6 @@ interface Props { updateNote: UpdateNote; updateTitle: UpdateTitle; usersViewing: string[]; - width: number; } const rightGutter = 60; // px @@ -49,7 +49,7 @@ const starIconWidth = 30; const nameWidth = 155; const descriptionWidth = 165; const noteWidth = 130; -const settingsWidth = 50; +const settingsWidth = 55; /** Displays the properties of a timeline, i.e. name, description, notes, etc */ export const Properties = React.memo( @@ -70,47 +70,36 @@ export const Properties = React.memo( updateNote, updateTitle, usersViewing, - width, }) => { + const { ref, width = 0 } = useThrottledResizeObserver(300); const [showActions, setShowActions] = useState(false); const [showNotes, setShowNotes] = useState(false); const [showTimelineModal, setShowTimelineModal] = useState(false); - const onButtonClick = useCallback(() => { - setShowActions(!showActions); - }, [showActions]); - - const onToggleShowNotes = useCallback(() => { - setShowNotes(!showNotes); - }, [showNotes]); - - const onClosePopover = useCallback(() => { - setShowActions(false); - }, []); - + const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); + const onToggleShowNotes = useCallback(() => setShowNotes(!showNotes), [showNotes]); + const onClosePopover = useCallback(() => setShowActions(false), []); + const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); + const onToggleLock = useCallback(() => toggleLock({ linkToId: 'timeline' }), [toggleLock]); const onOpenTimelineModal = useCallback(() => { onClosePopover(); setShowTimelineModal(true); }, []); - const onCloseTimelineModal = useCallback(() => { - setShowTimelineModal(false); - }, []); - - const datePickerWidth = - width - - rightGutter - - starIconWidth - - nameWidth - - (width >= showDescriptionThreshold ? descriptionWidth : 0) - - noteWidth - - settingsWidth; + const datePickerWidth = useMemo( + () => + width - + rightGutter - + starIconWidth - + nameWidth - + (width >= showDescriptionThreshold ? descriptionWidth : 0) - + noteWidth - + settingsWidth, + [width] + ); - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 return ( - + ( showNotesFromWidth={width >= showNotesThreshold} timelineId={timelineId} title={title} - toggleLock={() => { - toggleLock({ linkToId: 'timeline' }); - }} + toggleLock={onToggleLock} updateDescription={updateDescription} updateIsFavorite={updateIsFavorite} updateNote={updateNote} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx index 21800fefb21fb..3016def8a80b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx @@ -52,7 +52,15 @@ export const LockIconContainer = styled(EuiFlexItem)` LockIconContainer.displayName = 'LockIconContainer'; -export const DatePicker = styled(EuiFlexItem)` +interface WidthProp { + width: number; +} + +export const DatePicker = styled(EuiFlexItem).attrs(({ width }) => ({ + style: { + width: `${width}px`, + }, +}))` .euiSuperDatePicker__flexWrapper { max-width: none; width: auto; @@ -151,7 +159,7 @@ export const PropertiesLeft = React.memo( /> - + diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx index 3444875282ae7..74653fb6cb1ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx @@ -12,17 +12,26 @@ const fadeInEffect = keyframes` to { opacity: 1; } `; +interface WidthProp { + width: number; +} + export const TimelineProperties = styled.div` + flex: 1; align-items: center; display: flex; flex-direction: row; justify-content: space-between; user-select: none; `; + TimelineProperties.displayName = 'TimelineProperties'; -export const DatePicker = styled(EuiFlexItem)<{ width: number }>` - width: ${({ width }) => `${width}px`}; +export const DatePicker = styled(EuiFlexItem).attrs(({ width }) => ({ + style: { + width: `${width}px`, + }, +}))` .euiSuperDatePicker__flexWrapper { max-width: none; width: auto; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx index 3d2ec0683f091..73c20d9b9b6b4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx @@ -5,7 +5,7 @@ */ import React, { useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { inputsModel } from '../../store'; import { inputsActions } from '../../store/actions'; @@ -19,29 +19,20 @@ export interface TimelineRefetchProps { refetch: inputsModel.Refetch; } -type OwnProps = TimelineRefetchProps & PropsFromRedux; - -const TimelineRefetchComponent: React.FC = ({ +const TimelineRefetchComponent: React.FC = ({ id, inputId, inspect, loading, refetch, - setTimelineQuery, }) => { + const dispatch = useDispatch(); + useEffect(() => { - setTimelineQuery({ id, inputId, inspect, loading, refetch }); - }, [id, inputId, loading, refetch, inspect]); + dispatch(inputsActions.setQuery({ id, inputId, inspect, loading, refetch })); + }, [dispatch, id, inputId, loading, refetch, inspect]); return null; }; -const mapDispatchToProps = { - setTimelineQuery: inputsActions.setQuery, -}; - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const TimelineRefetch = connector(React.memo(TimelineRefetchComponent)); +export const TimelineRefetch = React.memo(TimelineRefetchComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx index d5e5d15eb8ad2..16fb57714829c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx @@ -11,12 +11,6 @@ import styled, { createGlobalStyle } from 'styled-components'; import { EventType } from '../../store/timeline/model'; import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; -/** - * OFFSET PIXEL VALUES - */ - -export const OFFSET_SCROLLBAR = 17; - /** * TIMELINE BODY */ @@ -30,10 +24,11 @@ export const TimelineBodyGlobalStyle = createGlobalStyle` export const TimelineBody = styled.div.attrs(({ className = '' }) => ({ className: `siemTimeline__body ${className}`, -}))<{ bodyHeight: number }>` - height: ${({ bodyHeight }) => `${bodyHeight}px`}; +}))<{ bodyHeight?: number }>` + height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; overflow: auto; scrollbar-width: thin; + flex: 1; &::-webkit-scrollbar { height: ${({ theme }) => theme.eui.euiScrollBar}; @@ -57,10 +52,19 @@ TimelineBody.displayName = 'TimelineBody'; * EVENTS TABLE */ -export const EventsTable = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsTable ${className}`, - role: 'table', -}))``; +interface EventsTableProps { + columnWidths: number; +} + +export const EventsTable = styled.div.attrs( + ({ className = '', columnWidths }) => ({ + className: `siemEventsTable ${className}`, + role: 'table', + style: { + minWidth: `${columnWidths}px`, + }, + }) +)``; /* EVENTS HEAD */ @@ -177,6 +181,14 @@ export const EventsTrData = styled.div.attrs(({ className = '' }) => ({ display: flex; `; +const TIMELINE_EVENT_DETAILS_OFFSET = 40; + +export const EventsTrSupplementContainer = styled.div.attrs(({ width }) => ({ + style: { + width: `${width! - TIMELINE_EVENT_DETAILS_OFFSET}px`, + }, +}))``; + export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__trSupplement ${className}`, }))<{ className: string }>` @@ -200,11 +212,17 @@ export const EventsTdGroupData = styled.div.attrs(({ className = '' }) => ({ }))` display: flex; `; +interface WidthProp { + width?: number; +} -export const EventsTd = styled.div.attrs(({ className = '' }) => ({ +export const EventsTd = styled.div.attrs(({ className = '', width }) => ({ className: `siemEventsTable__td ${className}`, role: 'cell', -}))` + style: { + flexBasis: width ? `${width}px` : 'auto', + }, +}))` align-items: center; display: flex; flex-shrink: 0; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index d66bc702bae43..ea4406311d7cc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -14,20 +14,17 @@ import { mockBrowserFields } from '../../containers/source/mock'; import { Direction } from '../../graphql/types'; import { defaultHeaders, mockTimelineData, mockIndexPattern } from '../../mock'; import { TestProviders } from '../../mock/test_providers'; -import { flyoutHeaderHeight } from '../flyout'; import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME, } from './data_providers/provider_item_actions'; -import { TimelineComponent } from './timeline'; +import { TimelineComponent, Props as TimelineComponentProps } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../utils/use_mount_appended'; -const testFlyoutHeight = 980; - jest.mock('../../lib/kibana'); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; @@ -35,6 +32,7 @@ jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); describe('Timeline', () => { + let props = {} as TimelineComponentProps; const sort: Sort = { columnId: '@timestamp', sortDirection: Direction.desc, @@ -50,41 +48,44 @@ describe('Timeline', () => { const mount = useMountAppended(); + beforeEach(() => { + props = { + browserFields: mockBrowserFields, + columns: defaultHeaders, + id: 'foo', + dataProviders: mockDataProviders, + end: endDate, + eventType: 'raw' as TimelineComponentProps['eventType'], + filters: [], + indexPattern, + indexToAdd: [], + isLive: false, + itemsPerPage: 5, + itemsPerPageOptions: [5, 10, 20], + kqlMode: 'search' as TimelineComponentProps['kqlMode'], + kqlQueryExpression: '', + loadingIndexName: false, + onChangeDataProviderKqlQuery: jest.fn(), + onChangeDroppableAndProvider: jest.fn(), + onChangeItemsPerPage: jest.fn(), + onClose: jest.fn(), + onDataProviderEdited: jest.fn(), + onDataProviderRemoved: jest.fn(), + onToggleDataProviderEnabled: jest.fn(), + onToggleDataProviderExcluded: jest.fn(), + show: true, + showCallOutUnauthorizedMsg: false, + start: startDate, + sort, + toggleColumn: jest.fn(), + usersViewing: ['elastic'], + }; + }); + describe('rendering', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); @@ -92,37 +93,7 @@ describe('Timeline', () => { const wrapper = mount( - + ); @@ -130,41 +101,28 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="timelineHeader"]').exists()).toEqual(true); }); + test('it renders the title field', () => { + const wrapper = mount( + + + + + + ); + + expect( + wrapper + .find('[data-test-subj="timeline-title"]') + .first() + .props().placeholder + ).toContain('Untitled Timeline'); + }); + test('it renders the timeline table', () => { const wrapper = mount( - + ); @@ -176,37 +134,7 @@ describe('Timeline', () => { const wrapper = mount( - + ); @@ -218,36 +146,7 @@ describe('Timeline', () => { const wrapper = mount( - + ); @@ -261,42 +160,10 @@ describe('Timeline', () => { describe('event wire up', () => { describe('onDataProviderRemoved', () => { test('it invokes the onDataProviderRemoved callback when the delete button on a provider is clicked', () => { - const mockOnDataProviderRemoved = jest.fn(); - const wrapper = mount( - + ); @@ -306,46 +173,16 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnDataProviderRemoved.mock.calls[0][0]).toEqual('id-Provider 1'); + expect((props.onDataProviderRemoved as jest.Mock).mock.calls[0][0]).toEqual( + 'id-Provider 1' + ); }); test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => { - const mockOnDataProviderRemoved = jest.fn(); - const wrapper = mount( - + ); @@ -361,48 +198,18 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnDataProviderRemoved.mock.calls[0][0]).toEqual('id-Provider 1'); + expect((props.onDataProviderRemoved as jest.Mock).mock.calls[0][0]).toEqual( + 'id-Provider 1' + ); }); }); describe('onToggleDataProviderEnabled', () => { test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => { - const mockOnToggleDataProviderEnabled = jest.fn(); - const wrapper = mount( - + ); @@ -419,7 +226,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderEnabled.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderEnabled as jest.Mock).mock.calls[0][0]).toEqual({ providerId: 'id-Provider 1', enabled: false, }); @@ -428,42 +235,10 @@ describe('Timeline', () => { describe('onToggleDataProviderExcluded', () => { test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => { - const mockOnToggleDataProviderExcluded = jest.fn(); - const wrapper = mount( - + ); @@ -482,7 +257,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderExcluded.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderExcluded as jest.Mock).mock.calls[0][0]).toEqual({ providerId: 'id-Provider 1', excluded: true, }); @@ -490,44 +265,14 @@ describe('Timeline', () => { }); describe('#ProviderWithAndProvider', () => { - test('Rendering And Provider', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); + const dataProviders = mockDataProviders.slice(0, 1); + dataProviders[0].and = mockDataProviders.slice(1, 3); + test('Rendering And Provider', () => { const wrapper = mount( - + ); @@ -544,44 +289,10 @@ describe('Timeline', () => { }); test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the accordion menu', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnDataProviderRemoved = jest.fn(); - const wrapper = mount( - + ); @@ -600,48 +311,17 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnDataProviderRemoved.mock.calls[0]).toEqual(['id-Provider 1', 'id-Provider 2']); + expect((props.onDataProviderRemoved as jest.Mock).mock.calls[0]).toEqual([ + 'id-Provider 1', + 'id-Provider 2', + ]); }); test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the accordion menu', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnToggleDataProviderEnabled = jest.fn(); - const wrapper = mount( - + ); @@ -660,7 +340,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderEnabled.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderEnabled as jest.Mock).mock.calls[0][0]).toEqual({ andProviderId: 'id-Provider 2', enabled: false, providerId: 'id-Provider 1', @@ -668,44 +348,10 @@ describe('Timeline', () => { }); test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the accordion menu', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnToggleDataProviderExcluded = jest.fn(); - const wrapper = mount( - + ); @@ -724,7 +370,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderExcluded.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderExcluded as jest.Mock).mock.calls[0][0]).toEqual({ andProviderId: 'id-Provider 2', excluded: true, providerId: 'id-Provider 1', diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index 58bbbef328ddf..098dd82791610 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; -import useResizeObserver from 'use-resize-observer/polyfilled'; +import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; import { Direction } from '../../graphql/types'; @@ -31,38 +31,60 @@ import { import { TimelineKqlFetch } from './fetch_kql_timeline'; import { Footer, footerHeight } from './footer'; import { TimelineHeader } from './header'; -import { calculateBodyHeight, combineQueries } from './helpers'; +import { combineQueries } from './helpers'; import { TimelineRefetch } from './refetch_timeline'; import { ManageTimelineContext } from './timeline_context'; import { esQuery, Filter, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -const WrappedByAutoSizer = styled.div` +const TimelineContainer = styled.div` + height: 100%; + display: flex; + flex-direction: column; +`; + +const TimelineHeaderContainer = styled.div` + margin-top: 6px; width: 100%; -`; // required by AutoSizer +`; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +TimelineHeaderContainer.displayName = 'TimelineHeaderContainer'; -const TimelineContainer = styled(EuiFlexGroup)` - min-height: 500px; - overflow: hidden; - padding: 0 10px 0 12px; - user-select: none; - width: 100%; +const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` + align-items: center; + box-shadow: none; + display: flex; + flex-direction: column; + padding: 14px 10px 0 12px; `; -TimelineContainer.displayName = 'TimelineContainer'; +const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` + overflow-y: hidden; + flex: 1; -export const isCompactFooter = (width: number): boolean => width < 600; + .euiFlyoutBody__overflow { + overflow: hidden; + mask-image: none; + } -interface Props { + .euiFlyoutBody__overflowContent { + padding: 0 10px 0 12px; + height: 100%; + display: flex; + } +`; + +const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)` + background: none; + padding: 0 10px 5px 12px; +`; + +export interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; dataProviders: DataProvider[]; end: number; eventType?: EventType; filters: Filter[]; - flyoutHeaderHeight: number; - flyoutHeight: number; id: string; indexPattern: IIndexPattern; indexToAdd: string[]; @@ -75,6 +97,7 @@ interface Props { onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery; onChangeDroppableAndProvider: OnChangeDroppableAndProvider; onChangeItemsPerPage: OnChangeItemsPerPage; + onClose: () => void; onDataProviderEdited: OnDataProviderEdited; onDataProviderRemoved: OnDataProviderRemoved; onToggleDataProviderEnabled: OnToggleDataProviderEnabled; @@ -84,6 +107,7 @@ interface Props { start: number; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; + usersViewing: string[]; } /** The parent Timeline component */ @@ -94,8 +118,6 @@ export const TimelineComponent: React.FC = ({ end, eventType, filters, - flyoutHeaderHeight, - flyoutHeight, id, indexPattern, indexToAdd, @@ -108,6 +130,7 @@ export const TimelineComponent: React.FC = ({ onChangeDataProviderKqlQuery, onChangeDroppableAndProvider, onChangeItemsPerPage, + onClose, onDataProviderEdited, onDataProviderRemoved, onToggleDataProviderEnabled, @@ -117,10 +140,8 @@ export const TimelineComponent: React.FC = ({ start, sort, toggleColumn, + usersViewing, }) => { - const { ref: measureRef, width = 0, height: timelineHeaderHeight = 0 } = useResizeObserver< - HTMLDivElement - >({}); const kibana = useKibana(); const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), @@ -134,45 +155,51 @@ export const TimelineComponent: React.FC = ({ end, }); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; + const timelineQueryFields = useMemo(() => columnsHeader.map(c => c.id), [columnsHeader]); + const timelineQuerySortField = useMemo( + () => ({ + sortFieldId: sort.columnId, + direction: sort.sortDirection as Direction, + }), + [sort.columnId, sort.sortDirection] + ); return ( - - }> - + + - + + + + {combinedQueries != null ? ( c.id)} + fields={timelineQueryFields} sourceId="default" limit={itemsPerPage} filterQuery={combinedQueries.filterQuery} - sortField={{ - sortFieldId: sort.columnId, - direction: sort.sortDirection as Direction, - }} + sortField={timelineQuerySortField} > {({ events, @@ -184,7 +211,7 @@ export const TimelineComponent: React.FC = ({ getUpdatedAt, refetch, }) => ( - + = ({ loading={loading} refetch={refetch} /> - -