diff --git a/.eslintignore b/.eslintignore index bbd8e3f88a37..4058d971b764 100644 --- a/.eslintignore +++ b/.eslintignore @@ -21,19 +21,13 @@ snapshots.js # plugin overrides /src/core/lib/kbn_internal_native_observable -/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken /src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/plugins/vis_type_timelion/common/_generated_/** -/x-pack/legacy/plugins/**/__tests__/fixtures/** /x-pack/plugins/apm/e2e/tmp/* /x-pack/plugins/canvas/canvas_plugin /x-pack/plugins/canvas/shareable_runtime/build /x-pack/plugins/canvas/storybook/build /x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/** -/x-pack/legacy/plugins/infra/common/graphql/types.ts -/x-pack/legacy/plugins/infra/public/graphql/types.ts -/x-pack/legacy/plugins/infra/server/graphql/types.ts -/x-pack/legacy/plugins/maps/public/vendor/** # package overrides /packages/elastic-eslint-config-kibana diff --git a/.eslintrc.js b/.eslintrc.js index 65c8e8ee2e69..19ba7cacc3c4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -410,11 +410,7 @@ module.exports = { errorMessage: `Common code can not import from server or public, use a common directory.`, }, { - target: [ - 'src/legacy/**/*', - '(src|x-pack)/plugins/**/(public|server)/**/*', - 'examples/**/*', - ], + target: ['(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*'], from: [ 'src/core/public/**/*', '!src/core/public/index.ts', // relative import @@ -428,8 +424,6 @@ module.exports = { '!src/core/server/mocks{,.ts}', '!src/core/server/types{,.ts}', '!src/core/server/test_utils{,.ts}', - '!src/core/server/utils', // ts alias - '!src/core/server/utils/**/*', // for absolute imports until fixed in // https://github.com/elastic/kibana/issues/36096 '!src/core/server/*.test.mocks{,.ts}', @@ -442,7 +436,6 @@ module.exports = { }, { target: [ - 'src/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*', '!(src|x-pack)/**/*.test.*', @@ -482,7 +475,7 @@ module.exports = { }, { target: ['src/core/**/*'], - from: ['plugins/**/*', 'src/plugins/**/*', 'src/legacy/ui/**/*'], + from: ['plugins/**/*', 'src/plugins/**/*'], errorMessage: 'The core cannot depend on any plugins.', }, { @@ -490,19 +483,6 @@ module.exports = { from: ['ui/**/*'], errorMessage: 'Plugins cannot import legacy UI code.', }, - { - from: ['src/legacy/ui/**/*', 'ui/**/*'], - target: [ - 'test/plugin_functional/plugins/**/public/np_ready/**/*', - 'test/plugin_functional/plugins/**/server/np_ready/**/*', - ], - allowSameFolder: true, - errorMessage: - 'NP-ready code should not import from /src/legacy/ui/** folder. ' + - 'Instead of importing from /src/legacy/ui/** deeply within a np_ready folder, ' + - 'import those things once at the top level of your plugin and pass those down, just ' + - 'like you pass down `core` and `plugins` objects.', - }, ], }, ], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d14556ea1dab..a8dcafeb7753 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -107,7 +107,6 @@ /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation -#CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-presentation #CC# /x-pack/plugins/dashboard_mode @elastic/kibana-presentation @@ -146,7 +145,6 @@ /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis -#CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis /src/plugins/tile_map/ @elastic/kibana-gis /src/plugins/region_map/ @elastic/kibana-gis @@ -165,7 +163,6 @@ /packages/kbn-utils/ @elastic/kibana-operations /packages/kbn-cli-dev-mode/ @elastic/kibana-operations /src/cli/keystore/ @elastic/kibana-operations -/src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /.github/workflows/ @elastic/kibana-operations /vars/ @elastic/kibana-operations @@ -202,9 +199,6 @@ /packages/kbn-legacy-logging/ @elastic/kibana-core /packages/kbn-crypto/ @elastic/kibana-core /packages/kbn-http-tools/ @elastic/kibana-core -/src/legacy/server/config/ @elastic/kibana-core -/src/legacy/server/http/ @elastic/kibana-core -/src/legacy/server/logging/ @elastic/kibana-core /src/plugins/status_page/ @elastic/kibana-core /src/plugins/saved_objects_management/ @elastic/kibana-core /src/dev/run_check_published_api_changes.ts @elastic/kibana-core @@ -214,9 +208,6 @@ /src/plugins/kibana_overview/ @elastic/kibana-core /x-pack/plugins/global_search_bar/ @elastic/kibana-core #CC# /src/core/server/csp/ @elastic/kibana-core -#CC# /src/legacy/server/config/ @elastic/kibana-core -#CC# /src/legacy/server/http/ @elastic/kibana-core -#CC# /src/legacy/ui/public/documentation_links @elastic/kibana-core #CC# /src/plugins/legacy_export/ @elastic/kibana-core #CC# /src/plugins/xpack_legacy/ @elastic/kibana-core #CC# /src/plugins/saved_objects/ @elastic/kibana-core diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index d9d2d6d1ddb8..37d04abda753 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -11,7 +11,7 @@ jobs: uses: elastic/github-actions/project-assigner@v2.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}]' + issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}], {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]' ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index bcf74936077e..691d7fb82f3b 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -452,10 +452,6 @@ using the CURL scripts in the scripts folder. |Visualize geo data from Elasticsearch or 3rd party geo-services. -|{kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] -|This plugin provides access to the detailed tile map services from Elastic. - - |{kib-repo}blob/{branch}/x-pack/plugins/ml/readme.md[ml] |This plugin provides access to the machine learning features provided by Elastic. diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md index 395c26a6e4bf..8ddc0da5f1b2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md @@ -10,10 +10,10 @@ Set of helpers used to create `KibanaResponse` to form HTTP response on an incom ```typescript kibanaResponseFactory: { - custom: | Error | Buffer | { + custom: | Error | Buffer | Stream | { message: string | Error; attributes?: Record | undefined; - } | Stream | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; + } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; badRequest: (options?: ErrorHttpResponseOptions) => KibanaResponse; unauthorized: (options?: ErrorHttpResponseOptions) => KibanaResponse; forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.core.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.core.md deleted file mode 100644 index 67f2cf0cdcc7..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.core.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [core](./kibana-plugin-core-server.legacyservicesetupdeps.core.md) - -## LegacyServiceSetupDeps.core property - -Signature: - -```typescript -core: LegacyCoreSetup; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md deleted file mode 100644 index a5c1d59be06d..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) - -## LegacyServiceSetupDeps interface - -> Warning: This API is now obsolete. -> -> - -Signature: - -```typescript -export interface LegacyServiceSetupDeps -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [core](./kibana-plugin-core-server.legacyservicesetupdeps.core.md) | LegacyCoreSetup | | -| [plugins](./kibana-plugin-core-server.legacyservicesetupdeps.plugins.md) | Record<string, unknown> | | -| [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) | UiPlugins | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.plugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.plugins.md deleted file mode 100644 index 032762904640..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.plugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [plugins](./kibana-plugin-core-server.legacyservicesetupdeps.plugins.md) - -## LegacyServiceSetupDeps.plugins property - -Signature: - -```typescript -plugins: Record; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md deleted file mode 100644 index d19a7dfcbfcf..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) - -## LegacyServiceSetupDeps.uiPlugins property - -Signature: - -```typescript -uiPlugins: UiPlugins; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.core.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.core.md deleted file mode 100644 index 17369e00a706..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.core.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) > [core](./kibana-plugin-core-server.legacyservicestartdeps.core.md) - -## LegacyServiceStartDeps.core property - -Signature: - -```typescript -core: LegacyCoreStart; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.md deleted file mode 100644 index d6f6b38b79f8..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) - -## LegacyServiceStartDeps interface - -> Warning: This API is now obsolete. -> -> - -Signature: - -```typescript -export interface LegacyServiceStartDeps -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [core](./kibana-plugin-core-server.legacyservicestartdeps.core.md) | LegacyCoreStart | | -| [plugins](./kibana-plugin-core-server.legacyservicestartdeps.plugins.md) | Record<string, unknown> | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.plugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.plugins.md deleted file mode 100644 index 4634bf21fb42..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.plugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) > [plugins](./kibana-plugin-core-server.legacyservicestartdeps.plugins.md) - -## LegacyServiceStartDeps.plugins property - -Signature: - -```typescript -plugins: Record; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index faac8108de82..3bbdf8c703ab 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -110,8 +110,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. | | [LegacyRequest](./kibana-plugin-core-server.legacyrequest.md) | | -| [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) | | -| [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) | | | [LoggerContextConfigInput](./kibana-plugin-core-server.loggercontextconfiginput.md) | | | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. | | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 025cab9f48c1..f4404521561d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc index fb4250368086..0218bac58815 100644 --- a/docs/maps/import-geospatial-data.asciidoc +++ b/docs/maps/import-geospatial-data.asciidoc @@ -6,6 +6,30 @@ To import geospatical data into the Elastic Stack, the data must be indexed as { Geospatial data comes in many formats. Choose an import tool based on the format of your geospatial data. +[discrete] +[[import-geospatial-privileges]] +=== Security privileges + +The {stack-security-features} provide roles and privileges that control which users can upload files. +You can manage your roles, privileges, and +spaces in **{stack-manage-app}** in {kib}. For more information, see +{ref}/security-privileges.html[Security privileges], +<>, and <>. + +To upload GeoJSON files in {kib} with *Maps*, you must have: + +* The `all` {kib} privilege for *Maps*. +* The `all` {kib} privilege for *Index Pattern Management*. +* The `create` and `create_index` index privileges for destination indices. +* To use the index in *Maps*, you must also have the `read` and `view_index_metadata` index privileges for destination indices. + +To upload CSV files in {kib} with the *{file-data-viz}*, you must have privileges to upload GeoJSON files and: + +* The `manage_pipeline` cluster privilege. +* The `read` {kib} privilege for *Machine Learning*. +* The `machine_learning_admin` or `machine_learning_user` role. + + [discrete] === Upload CSV with latitude and longitude columns diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index acb343191609..d62e3c3eb88a 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -52,7 +52,7 @@ for example, `logstash-*`. ==== Default logging timezone is now the system's timezone *Details:* In prior releases the timezone used in logs defaulted to UTC. We now use the host machine's timezone by default. -*Impact:* To restore the previous behavior, in kibana.yml use the pattern layout, with a date modifier: +*Impact:* To restore the previous behavior, in kibana.yml use the pattern layout, with a {kibana-ref}/logging-service.html#date-format[date modifier]: [source,yaml] ------------------- logging: @@ -87,7 +87,7 @@ See https://github.com/elastic/kibana/pull/87939 for more details. [float] ==== Logging destination is specified by the appender -*Details:* Previously log destination would be `stdout` and could be changed to `file` using `logging.dest`. With the new logging configuration, you can specify the destination using appenders. +*Details:* Previously log destination would be `stdout` and could be changed to `file` using `logging.dest`. With the new logging configuration, you can specify the destination using {kibana-ref}/logging-service.html#logging-appenders[appenders]. *Impact:* To restore the previous behavior and log records to *stdout*, in `kibana.yml` use an appender with `type: console`. [source,yaml] @@ -118,7 +118,7 @@ logging: [float] ==== Set log verbosity with root -*Details:* Previously logging output would be specified by `logging.silent` (none), `logging.quiet` (error messages only) and `logging.verbose` (all). With the new logging configuration, set the minimum required log level. +*Details:* Previously logging output would be specified by `logging.silent` (none), `logging.quiet` (error messages only) and `logging.verbose` (all). With the new logging configuration, set the minimum required {kibana-ref}/logging-service.html#log-level[log level]. *Impact:* To restore the previous behavior, in `kibana.yml` specify `logging.root.level`: [source,yaml] @@ -175,7 +175,7 @@ logging: ==== Configure log rotation with the rolling-file appender *Details:* Previously log rotation would be enabled when `logging.rotate.enabled` was true. -*Impact:* To restore the previous behavior, in `kibana.yml` use the `rolling-file` appender. +*Impact:* To restore the previous behavior, in `kibana.yml` use the {kibana-ref}/logging-service.html#rolling-file-appender[`rolling-file`] appender. [source,yaml] ------------------- diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 73b268e1e48b..643718b96165 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -281,7 +281,7 @@ To reload the logging settings, send a SIGHUP signal to {kib}. |=== |[[logging-root]] `logging.root:` -| The `root` logger has a dedicated configuration node since this context name is special and is pre-configured for logging by default. +| The {kibana-ref}/logging-service.html#logging-service[`root` logger] has a dedicated configuration node since this context name is special and is pre-configured for logging by default. // TODO: add link to the advanced logging documentation. |[[logging-root-appenders]] `logging.root.appenders:` @@ -303,7 +303,7 @@ To reload the logging settings, send a SIGHUP signal to {kib}. | Specific appender format to apply for a particular logger context. | `logging.appenders:` -| Define how and where log messages are displayed (eg. *stdout* or console) and stored (eg. file on the disk). +| {kibana-ref}/logging-service.html#logging-appenders[Appenders] define how and where log messages are displayed (eg. *stdout* or console) and stored (eg. file on the disk). // TODO: add link to the advanced logging documentation. | `logging.appenders.console:` diff --git a/jest.config.js b/jest.config.js index 03dc832ba170..bd1e865a7e64 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,6 @@ module.exports = { projects: [ '/packages/*/jest.config.js', '/src/*/jest.config.js', - '/src/legacy/*/jest.config.js', '/src/plugins/*/jest.config.js', '/test/*/jest.config.js', '/x-pack/plugins/*/jest.config.js', diff --git a/kibana.d.ts b/kibana.d.ts index a2c670c96a69..8a7a53189005 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -13,18 +13,3 @@ import * as Public from 'src/core/public'; import * as Server from 'src/core/server'; export { Public, Server }; - -/** - * All exports from TS ambient definitions (where types are added for JS source in a .d.ts file). - */ -import * as LegacyKibanaServer from './src/legacy/server/kbn_server'; - -/** - * Re-export legacy types under a namespace. - */ -export namespace Legacy { - export type KibanaConfig = LegacyKibanaServer.KibanaConfig; - export type Request = LegacyKibanaServer.Request; - export type ResponseToolkit = LegacyKibanaServer.ResponseToolkit; - export type Server = LegacyKibanaServer.Server; -} diff --git a/package.json b/package.json index 34e044140d29..d79df127a7d3 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "**/cross-fetch/node-fetch": "^2.6.1", "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", + "globby/fast-glob": "3.2.5", "**/graphql-toolkit/lodash": "^4.17.21", "**/hoist-non-react-statics": "^3.3.2", "**/isomorphic-fetch/node-fetch": "^2.6.1", @@ -97,7 +98,7 @@ "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "27.0.0", + "@elastic/charts": "28.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath/npm_module", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.12.0", @@ -240,7 +241,7 @@ "github-markdown-css": "^2.10.0", "glob": "^7.1.2", "glob-all": "^3.2.1", - "globby": "^8.0.1", + "globby": "^11.0.3", "graphql": "^0.13.2", "graphql-fields": "^1.0.2", "graphql-tag": "^2.10.3", @@ -443,7 +444,7 @@ "@bazel/ibazel": "^0.14.0", "@bazel/typescript": "^3.2.3", "@cypress/snapshot": "^2.1.7", - "@cypress/webpack-preprocessor": "^5.5.0", + "@cypress/webpack-preprocessor": "^5.6.0", "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", "@elastic/eslint-config-kibana": "link:packages/elastic-eslint-config-kibana", @@ -534,7 +535,6 @@ "@types/getos": "^3.0.0", "@types/git-url-parse": "^9.0.0", "@types/glob": "^7.1.2", - "@types/globby": "^8.0.0", "@types/graphql": "^0.13.2", "@types/gulp": "^4.0.6", "@types/gulp-zip": "^4.0.1", @@ -682,7 +682,7 @@ "copy-webpack-plugin": "^6.0.2", "cpy": "^8.1.1", "css-loader": "^3.4.2", - "cypress": "^6.2.1", + "cypress": "^6.8.0", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", "cypress-pipe": "^2.0.0", diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts index ab113b96a5f0..ff25f2a7bf55 100644 --- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts @@ -27,8 +27,6 @@ it('produces the right watch and ignore list', () => { expect(watchPaths).toMatchInlineSnapshot(` Array [ /src/core, - /src/legacy/server, - /src/legacy/utils, /config, /x-pack/test/plugin_functional/plugins/resolver_test, /src/plugins, diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 2801e0a0688c..17ac75e9f3d9 100644 --- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -1,69 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#get correctly handles server config.: default 1`] = ` -Object { - "autoListen": true, - "basePath": "/abc", - "compression": Object { - "enabled": true, - }, - "cors": false, - "customResponseHeaders": Object { - "custom-header": "custom-value", - }, - "host": "host", - "keepaliveTimeout": 5000, - "maxPayload": 1000, - "name": "kibana-hostname", - "port": 1234, - "publicBaseUrl": "https://myhost.com/abc", - "rewriteBasePath": false, - "socketTimeout": 2000, - "ssl": Object { - "enabled": true, - "keyPassphrase": "some-phrase", - "someNewValue": "new", - }, - "uuid": undefined, - "xsrf": Object { - "allowlist": Array [], - "disableProtection": false, - }, -} -`; - -exports[`#get correctly handles server config.: disabled ssl 1`] = ` -Object { - "autoListen": true, - "basePath": "/abc", - "compression": Object { - "enabled": true, - }, - "cors": false, - "customResponseHeaders": Object { - "custom-header": "custom-value", - }, - "host": "host", - "keepaliveTimeout": 5000, - "maxPayload": 1000, - "name": "kibana-hostname", - "port": 1234, - "publicBaseUrl": "http://myhost.com/abc", - "rewriteBasePath": false, - "socketTimeout": 2000, - "ssl": Object { - "certificate": "cert", - "enabled": false, - "key": "key", - }, - "uuid": undefined, - "xsrf": Object { - "allowlist": Array [], - "disableProtection": false, - }, -} -`; - exports[`#get correctly handles silent logging config. 1`] = ` Object { "appenders": Object { @@ -78,6 +14,7 @@ Object { "root": Object { "level": "off", }, + "silent": true, } `; @@ -93,10 +30,13 @@ Object { "type": "legacy-appender", }, }, + "dest": "/some/path.log", + "json": true, "loggers": undefined, "root": Object { "level": "all", }, + "verbose": true, } `; diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts index 5dd194154570..47151503e163 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts @@ -65,59 +65,6 @@ describe('#get', () => { expect(configAdapter.get('logging')).toMatchSnapshot(); }); - - test('correctly handles server config.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - server: { - name: 'kibana-hostname', - autoListen: true, - basePath: '/abc', - cors: false, - customResponseHeaders: { 'custom-header': 'custom-value' }, - host: 'host', - maxPayloadBytes: 1000, - keepaliveTimeout: 5000, - socketTimeout: 2000, - port: 1234, - publicBaseUrl: 'https://myhost.com/abc', - rewriteBasePath: false, - ssl: { enabled: true, keyPassphrase: 'some-phrase', someNewValue: 'new' }, - compression: { enabled: true }, - someNotSupportedValue: 'val', - xsrf: { - disableProtection: false, - allowlist: [], - }, - }, - }); - - const configAdapterWithDisabledSSL = new LegacyObjectToConfigAdapter({ - server: { - name: 'kibana-hostname', - autoListen: true, - basePath: '/abc', - cors: false, - customResponseHeaders: { 'custom-header': 'custom-value' }, - host: 'host', - maxPayloadBytes: 1000, - keepaliveTimeout: 5000, - socketTimeout: 2000, - port: 1234, - publicBaseUrl: 'http://myhost.com/abc', - rewriteBasePath: false, - ssl: { enabled: false, certificate: 'cert', key: 'key' }, - compression: { enabled: true }, - someNotSupportedValue: 'val', - xsrf: { - disableProtection: false, - allowlist: [], - }, - }, - }); - - expect(configAdapter.get('server')).toMatchSnapshot('default'); - expect(configAdapterWithDisabledSSL.get('server')).toMatchSnapshot('disabled ssl'); - }); }); describe('#set', () => { diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts index 8ec26ff1f8e7..bc6fd49e2498 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts @@ -9,15 +9,6 @@ import { ConfigPath } from '../config'; import { ObjectToConfigAdapter } from '../object_to_config_adapter'; -// TODO: fix once core schemas are moved to this package -type LoggingConfigType = any; - -/** - * @internal - * @deprecated - */ -export type LegacyVars = Record; - /** * Represents logging config supported by the legacy platform. */ @@ -30,7 +21,7 @@ export interface LegacyLoggingConfig { events?: Record; } -type MixedLoggingConfig = LegacyLoggingConfig & Partial; +type MixedLoggingConfig = LegacyLoggingConfig & Record; /** * Represents adapter between config provided by legacy platform and `Config` @@ -48,6 +39,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { }, root: { level: 'info', ...root }, loggers, + ...legacyLoggingConfig, }; if (configValue.silent) { @@ -61,47 +53,11 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { return loggingConfig; } - private static transformServer(configValue: any = {}) { - // TODO: New platform uses just a subset of `server` config from the legacy platform, - // new values will be exposed once we need them - return { - autoListen: configValue.autoListen, - basePath: configValue.basePath, - cors: configValue.cors, - customResponseHeaders: configValue.customResponseHeaders, - host: configValue.host, - maxPayload: configValue.maxPayloadBytes, - name: configValue.name, - port: configValue.port, - publicBaseUrl: configValue.publicBaseUrl, - rewriteBasePath: configValue.rewriteBasePath, - ssl: configValue.ssl, - keepaliveTimeout: configValue.keepaliveTimeout, - socketTimeout: configValue.socketTimeout, - compression: configValue.compression, - uuid: configValue.uuid, - xsrf: configValue.xsrf, - }; - } - - private static transformPlugins(configValue: LegacyVars = {}) { - // These properties are the only ones we use from the existing `plugins` config node - // since `scanDirs` isn't respected by new platform plugin discovery. - return { - initialize: configValue.initialize, - paths: configValue.paths, - }; - } - public get(configPath: ConfigPath) { const configValue = super.get(configPath); switch (configPath) { case 'logging': return LegacyObjectToConfigAdapter.transformLogging(configValue as LegacyLoggingConfig); - case 'server': - return LegacyObjectToConfigAdapter.transformServer(configValue); - case 'plugins': - return LegacyObjectToConfigAdapter.transformPlugins(configValue as LegacyVars); default: return configValue; } diff --git a/packages/kbn-legacy-logging/package.json b/packages/kbn-legacy-logging/package.json index 9450fd39607e..96edeccad665 100644 --- a/packages/kbn-legacy-logging/package.json +++ b/packages/kbn-legacy-logging/package.json @@ -11,6 +11,7 @@ "kbn:watch": "yarn build --watch" }, "dependencies": { - "@kbn/utils": "link:../kbn-utils" + "@kbn/utils": "link:../kbn-utils", + "@kbn/config-schema": "link:../kbn-config-schema" } } diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts index e1edd06a4b4a..3ece0f6f1ee4 100644 --- a/packages/kbn-legacy-logging/src/legacy_logging_server.ts +++ b/packages/kbn-legacy-logging/src/legacy_logging_server.ts @@ -88,7 +88,7 @@ export class LegacyLoggingServer { // We set `ops.interval` to max allowed number and `ops` filter to value // that doesn't exist to avoid logging of ops at all, if turned on it will be // logged by the "legacy" Kibana. - const { value: loggingConfig } = legacyLoggingConfigSchema.validate({ + const loggingConfig = legacyLoggingConfigSchema.validate({ ...legacyLoggingConfig, events: { ...legacyLoggingConfig.events, diff --git a/packages/kbn-legacy-logging/src/schema.ts b/packages/kbn-legacy-logging/src/schema.ts index 76d7381ee872..0330708e746c 100644 --- a/packages/kbn-legacy-logging/src/schema.ts +++ b/packages/kbn-legacy-logging/src/schema.ts @@ -6,11 +6,8 @@ * Side Public License, v 1. */ -import Joi from 'joi'; +import { schema } from '@kbn/config-schema'; -const HANDLED_IN_KIBANA_PLATFORM = Joi.any().description( - 'This key is handled in the new platform ONLY' -); /** * @deprecated * @@ -36,46 +33,65 @@ export interface LegacyLoggingConfig { }; } -export const legacyLoggingConfigSchema = Joi.object() - .keys({ - appenders: HANDLED_IN_KIBANA_PLATFORM, - loggers: HANDLED_IN_KIBANA_PLATFORM, - root: HANDLED_IN_KIBANA_PLATFORM, - - silent: Joi.boolean().default(false), - quiet: Joi.boolean().when('silent', { - is: true, - then: Joi.boolean().default(true).valid(true), - otherwise: Joi.boolean().default(false), +export const legacyLoggingConfigSchema = schema.object({ + silent: schema.boolean({ defaultValue: false }), + quiet: schema.conditional( + schema.siblingRef('silent'), + true, + schema.boolean({ + defaultValue: true, + validate: (quiet) => { + if (!quiet) { + return 'must be true when `silent` is true'; + } + }, + }), + schema.boolean({ defaultValue: false }) + ), + verbose: schema.conditional( + schema.siblingRef('quiet'), + true, + schema.boolean({ + defaultValue: false, + validate: (verbose) => { + if (verbose) { + return 'must be false when `quiet` is true'; + } + }, + }), + schema.boolean({ defaultValue: false }) + ), + events: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + dest: schema.string({ defaultValue: 'stdout' }), + filter: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + json: schema.conditional( + schema.siblingRef('dest'), + 'stdout', + schema.boolean({ + defaultValue: !process.stdout.isTTY, + }), + schema.boolean({ + defaultValue: true, + }) + ), + timezone: schema.maybe(schema.string()), + rotate: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + everyBytes: schema.number({ + min: 1048576, // > 1MB + max: 1073741825, // < 1GB + defaultValue: 10485760, // 10MB }), - verbose: Joi.boolean().when('quiet', { - is: true, - then: Joi.valid(false).default(false), - otherwise: Joi.boolean().default(false), + keepFiles: schema.number({ + min: 2, + max: 1024, + defaultValue: 7, }), - events: Joi.any().default({}), - dest: Joi.string().default('stdout'), - filter: Joi.any().default({}), - json: Joi.boolean().when('dest', { - is: 'stdout', - then: Joi.boolean().default(!process.stdout.isTTY), - otherwise: Joi.boolean().default(true), + pollingInterval: schema.number({ + min: 5000, + max: 3600000, + defaultValue: 10000, }), - timezone: Joi.string(), - rotate: Joi.object() - .keys({ - enabled: Joi.boolean().default(false), - everyBytes: Joi.number() - // > 1MB - .greater(1048576) - // < 1GB - .less(1073741825) - // 10MB - .default(10485760), - keepFiles: Joi.number().greater(2).less(1024).default(7), - pollingInterval: Joi.number().greater(5000).less(3600000).default(10000), - usePolling: Joi.boolean().default(false), - }) - .default(), - }) - .default(); + usePolling: schema.boolean({ defaultValue: false }), + }), +}); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 3c9fd4f59a40..249183d4b1e3 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -46,12 +46,11 @@ pageLoadAssetSize: lens: 96624 licenseManagement: 41817 licensing: 29004 - lists: 202261 + lists: 228500 logstash: 53548 management: 46112 maps: 80000 mapsLegacy: 87859 - mapsLegacyLicensing: 20214 ml: 82187 monitoring: 80000 navigation: 37269 @@ -69,7 +68,7 @@ pageLoadAssetSize: searchprofiler: 67080 security: 189428 securityOss: 30806 - securitySolution: 283440 + securitySolution: 235402 share: 99061 snapshotRestore: 79032 spaces: 387915 diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index c546a0c6cf99..8becc76a23ca 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -457,7 +457,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 22, + 25, ], "results": Array [ Object { @@ -480,7 +480,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 25, + 28, ], "results": Array [ Object { @@ -505,7 +505,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 23, + 26, ], "results": Array [ Object { diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 509ce89f8c02..7c5d0390d9fb 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -14455,6 +14455,7 @@ module.exports = FastGlob; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.convertPatternGroupToTask = exports.convertPatternGroupsToTasks = exports.groupPatternsByBaseDirectory = exports.getNegativePatternsAsPositive = exports.getPositivePatterns = exports.convertPatternsToTasks = exports.generate = void 0; const utils = __webpack_require__(165); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); @@ -14526,6 +14527,7 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.string = exports.stream = exports.pattern = exports.path = exports.fs = exports.errno = exports.array = void 0; const array = __webpack_require__(166); exports.array = array; const errno = __webpack_require__(167); @@ -14549,6 +14551,7 @@ exports.string = string; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.splitWhen = exports.flatten = void 0; function flatten(items) { return items.reduce((collection, item) => [].concat(collection, item), []); } @@ -14577,6 +14580,7 @@ exports.splitWhen = splitWhen; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEnoentCodeError = void 0; function isEnoentCodeError(error) { return error.code === 'ENOENT'; } @@ -14590,6 +14594,7 @@ exports.isEnoentCodeError = isEnoentCodeError; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.createDirentFromStats = void 0; class DirentFromStats { constructor(name, stats) { this.name = name; @@ -14615,6 +14620,7 @@ exports.createDirentFromStats = createDirentFromStats; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.removeLeadingDotSegment = exports.escape = exports.makeAbsolute = exports.unixify = void 0; const path = __webpack_require__(4); const LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2; // ./ or .\\ const UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\())/g; @@ -14654,6 +14660,7 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0; const path = __webpack_require__(4); const globParent = __webpack_require__(171); const micromatch = __webpack_require__(174); @@ -14670,6 +14677,14 @@ function isStaticPattern(pattern, options = {}) { } exports.isStaticPattern = isStaticPattern; function isDynamicPattern(pattern, options = {}) { + /** + * A special case with an empty string is necessary for matching patterns that start with a forward slash. + * An empty string cannot be a dynamic pattern. + * For example, the pattern `/lib/*` will be spread into parts: '', 'lib', '*'. + */ + if (pattern === '') { + return false; + } /** * When the `caseSensitiveMatch` option is disabled, all patterns must be marked as dynamic, because we cannot check * filepath directly (without read directory). @@ -14744,12 +14759,23 @@ function expandBraceExpansion(pattern) { } exports.expandBraceExpansion = expandBraceExpansion; function getPatternParts(pattern, options) { - const info = picomatch.scan(pattern, Object.assign(Object.assign({}, options), { parts: true })); - // See micromatch/picomatch#58 for more details - if (info.parts.length === 0) { - return [pattern]; + let { parts } = picomatch.scan(pattern, Object.assign(Object.assign({}, options), { parts: true })); + /** + * The scan method returns an empty array in some cases. + * See micromatch/picomatch#58 for more details. + */ + if (parts.length === 0) { + parts = [pattern]; + } + /** + * The scan method does not return an empty part for the pattern with a forward slash. + * This is another part of micromatch/picomatch#58. + */ + if (parts[0].startsWith('/')) { + parts[0] = parts[0].slice(1); + parts.unshift(''); } - return info.parts; + return parts; } exports.getPatternParts = getPatternParts; function makeRe(pattern, options) { @@ -18963,6 +18989,7 @@ module.exports = parse; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.merge = void 0; const merge2 = __webpack_require__(146); function merge(streams) { const mergedStream = merge2(streams); @@ -18986,6 +19013,7 @@ function propagateCloseEventToSources(streams) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEmpty = exports.isString = void 0; function isString(input) { return typeof input === 'string'; } @@ -20314,8 +20342,7 @@ class DeepFilter { return utils.pattern.convertPatternsToRe(affectDepthOfReadingPatterns, this._micromatchOptions); } _filter(basePath, entry, matcher, negativeRe) { - const depth = this._getEntryLevel(basePath, entry.path); - if (this._isSkippedByDeep(depth)) { + if (this._isSkippedByDeep(basePath, entry.path)) { return false; } if (this._isSkippedSymbolicLink(entry)) { @@ -20327,22 +20354,31 @@ class DeepFilter { } return this._isSkippedByNegativePatterns(filepath, negativeRe); } - _isSkippedByDeep(entryDepth) { - return entryDepth >= this._settings.deep; - } - _isSkippedSymbolicLink(entry) { - return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink(); + _isSkippedByDeep(basePath, entryPath) { + /** + * Avoid unnecessary depth calculations when it doesn't matter. + */ + if (this._settings.deep === Infinity) { + return false; + } + return this._getEntryLevel(basePath, entryPath) >= this._settings.deep; } _getEntryLevel(basePath, entryPath) { - const basePathDepth = basePath.split('/').length; const entryPathDepth = entryPath.split('/').length; - return entryPathDepth - (basePath === '' ? 0 : basePathDepth); + if (basePath === '') { + return entryPathDepth; + } + const basePathDepth = basePath.split('/').length; + return entryPathDepth - basePathDepth; + } + _isSkippedSymbolicLink(entry) { + return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink(); } _isSkippedByPositivePatterns(entryPath, matcher) { return !this._settings.baseNameMatch && !matcher.match(entryPath); } - _isSkippedByNegativePatterns(entryPath, negativeRe) { - return !utils.pattern.matchAny(entryPath, negativeRe); + _isSkippedByNegativePatterns(entryPath, patternsRe) { + return !utils.pattern.matchAny(entryPath, patternsRe); } } exports.default = DeepFilter; @@ -20470,20 +20506,21 @@ class EntryFilter { return (entry) => this._filter(entry, positiveRe, negativeRe); } _filter(entry, positiveRe, negativeRe) { - if (this._settings.unique) { - if (this._isDuplicateEntry(entry)) { - return false; - } - this._createIndexRecord(entry); + if (this._settings.unique && this._isDuplicateEntry(entry)) { + return false; } if (this._onlyFileFilter(entry) || this._onlyDirectoryFilter(entry)) { return false; } - if (this._isSkippedByAbsoluteNegativePatterns(entry, negativeRe)) { + if (this._isSkippedByAbsoluteNegativePatterns(entry.path, negativeRe)) { return false; } const filepath = this._settings.baseNameMatch ? entry.name : entry.path; - return this._isMatchToPatterns(filepath, positiveRe) && !this._isMatchToPatterns(entry.path, negativeRe); + const isMatched = this._isMatchToPatterns(filepath, positiveRe) && !this._isMatchToPatterns(entry.path, negativeRe); + if (this._settings.unique && isMatched) { + this._createIndexRecord(entry); + } + return isMatched; } _isDuplicateEntry(entry) { return this.index.has(entry.path); @@ -20497,12 +20534,12 @@ class EntryFilter { _onlyDirectoryFilter(entry) { return this._settings.onlyDirectories && !entry.dirent.isDirectory(); } - _isSkippedByAbsoluteNegativePatterns(entry, negativeRe) { + _isSkippedByAbsoluteNegativePatterns(entryPath, patternsRe) { if (!this._settings.absolute) { return false; } - const fullpath = utils.path.makeAbsolute(this._settings.cwd, entry.path); - return this._isMatchToPatterns(fullpath, negativeRe); + const fullpath = utils.path.makeAbsolute(this._settings.cwd, entryPath); + return utils.pattern.matchAny(fullpath, patternsRe); } _isMatchToPatterns(entryPath, patternsRe) { const filepath = utils.path.removeLeadingDotSegment(entryPath); @@ -20692,9 +20729,14 @@ exports.default = ReaderSync; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.DEFAULT_FILE_SYSTEM_ADAPTER = void 0; const fs = __webpack_require__(134); const os = __webpack_require__(121); -const CPU_COUNT = os.cpus().length; +/** + * The `os.cpus` method can return zero. We expect the number of cores to be greater than zero. + * https://github.com/nodejs/node/blob/7faeddf23a98c53896f8b574a6e66589e8fb1eb8/lib/os.js#L106-L107 + */ +const CPU_COUNT = Math.max(os.cpus().length, 1); exports.DEFAULT_FILE_SYSTEM_ADAPTER = { lstat: fs.lstat, lstatSync: fs.lstatSync, @@ -63636,7 +63678,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(564); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildBazelProductionProjects"]; }); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(783); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(812); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); /* @@ -63662,7 +63704,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(globby__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(783); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(812); /* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(372); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(131); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); @@ -90264,131 +90306,184 @@ module.exports = CpyError; "use strict"; -const arrayUnion = __webpack_require__(775); -const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(572); -const dirGlob = __webpack_require__(776); -const gitignore = __webpack_require__(780); +const fs = __webpack_require__(134); +const arrayUnion = __webpack_require__(145); +const merge2 = __webpack_require__(146); +const fastGlob = __webpack_require__(775); +const dirGlob = __webpack_require__(232); +const gitignore = __webpack_require__(810); +const {FilterStream, UniqueStream} = __webpack_require__(811); const DEFAULT_FILTER = () => false; const isNegative = pattern => pattern[0] === '!'; const assertPatternsInput = patterns => { - if (!patterns.every(x => typeof x === 'string')) { + if (!patterns.every(pattern => typeof pattern === 'string')) { throw new TypeError('Patterns must be a string or an array of strings'); } }; -const generateGlobTasks = (patterns, taskOpts) => { - patterns = [].concat(patterns); +const checkCwdOption = (options = {}) => { + if (!options.cwd) { + return; + } + + let stat; + try { + stat = fs.statSync(options.cwd); + } catch { + return; + } + + if (!stat.isDirectory()) { + throw new Error('The `cwd` option must be a path to a directory'); + } +}; + +const getPathString = p => p.stats instanceof fs.Stats ? p.path : p; + +const generateGlobTasks = (patterns, taskOptions) => { + patterns = arrayUnion([].concat(patterns)); assertPatternsInput(patterns); + checkCwdOption(taskOptions); const globTasks = []; - taskOpts = Object.assign({ + taskOptions = { ignore: [], - expandDirectories: true - }, taskOpts); + expandDirectories: true, + ...taskOptions + }; - patterns.forEach((pattern, i) => { + for (const [index, pattern] of patterns.entries()) { if (isNegative(pattern)) { - return; + continue; } const ignore = patterns - .slice(i) - .filter(isNegative) + .slice(index) + .filter(pattern => isNegative(pattern)) .map(pattern => pattern.slice(1)); - const opts = Object.assign({}, taskOpts, { - ignore: taskOpts.ignore.concat(ignore) - }); + const options = { + ...taskOptions, + ignore: taskOptions.ignore.concat(ignore) + }; - globTasks.push({pattern, opts}); - }); + globTasks.push({pattern, options}); + } return globTasks; }; const globDirs = (task, fn) => { - let opts = {cwd: task.opts.cwd}; + let options = {}; + if (task.options.cwd) { + options.cwd = task.options.cwd; + } - if (Array.isArray(task.opts.expandDirectories)) { - opts = Object.assign(opts, {files: task.opts.expandDirectories}); - } else if (typeof task.opts.expandDirectories === 'object') { - opts = Object.assign(opts, task.opts.expandDirectories); + if (Array.isArray(task.options.expandDirectories)) { + options = { + ...options, + files: task.options.expandDirectories + }; + } else if (typeof task.options.expandDirectories === 'object') { + options = { + ...options, + ...task.options.expandDirectories + }; } - return fn(task.pattern, opts); + return fn(task.pattern, options); }; -const getPattern = (task, fn) => task.opts.expandDirectories ? globDirs(task, fn) : [task.pattern]; +const getPattern = (task, fn) => task.options.expandDirectories ? globDirs(task, fn) : [task.pattern]; -module.exports = (patterns, opts) => { - let globTasks; +const getFilterSync = options => { + return options && options.gitignore ? + gitignore.sync({cwd: options.cwd, ignore: options.ignore}) : + DEFAULT_FILTER; +}; - try { - globTasks = generateGlobTasks(patterns, opts); - } catch (err) { - return Promise.reject(err); +const globToTask = task => glob => { + const {options} = task; + if (options.ignore && Array.isArray(options.ignore) && options.expandDirectories) { + options.ignore = dirGlob.sync(options.ignore); } - const getTasks = Promise.all(globTasks.map(task => Promise.resolve(getPattern(task, dirGlob)) - .then(globs => Promise.all(globs.map(glob => ({ - pattern: glob, - opts: task.opts - })))) - )) - .then(tasks => arrayUnion.apply(null, tasks)); - - const getFilter = () => { - return Promise.resolve( - opts && opts.gitignore ? - gitignore({cwd: opts.cwd, ignore: opts.ignore}) : - DEFAULT_FILTER - ); + return { + pattern: glob, + options }; - - return getFilter() - .then(filter => { - return getTasks - .then(tasks => Promise.all(tasks.map(task => fastGlob(task.pattern, task.opts)))) - .then(paths => arrayUnion.apply(null, paths)) - .then(paths => paths.filter(p => !filter(p))); - }); }; -module.exports.sync = (patterns, opts) => { - const globTasks = generateGlobTasks(patterns, opts); +module.exports = async (patterns, options) => { + const globTasks = generateGlobTasks(patterns, options); - const getFilter = () => { - return opts && opts.gitignore ? - gitignore.sync({cwd: opts.cwd, ignore: opts.ignore}) : + const getFilter = async () => { + return options && options.gitignore ? + gitignore({cwd: options.cwd, ignore: options.ignore}) : DEFAULT_FILTER; }; - const tasks = globTasks.reduce((tasks, task) => { - const newTask = getPattern(task, dirGlob.sync).map(glob => ({ - pattern: glob, - opts: task.opts + const getTasks = async () => { + const tasks = await Promise.all(globTasks.map(async task => { + const globs = await getPattern(task, dirGlob); + return Promise.all(globs.map(globToTask(task))); })); - return tasks.concat(newTask); - }, []); - const filter = getFilter(); + return arrayUnion(...tasks); + }; - return tasks.reduce( - (matches, task) => arrayUnion(matches, fastGlob.sync(task.pattern, task.opts)), - [] - ).filter(p => !filter(p)); + const [filter, tasks] = await Promise.all([getFilter(), getTasks()]); + const paths = await Promise.all(tasks.map(task => fastGlob(task.pattern, task.options))); + + return arrayUnion(...paths).filter(path_ => !filter(getPathString(path_))); +}; + +module.exports.sync = (patterns, options) => { + const globTasks = generateGlobTasks(patterns, options); + + const tasks = []; + for (const task of globTasks) { + const newTask = getPattern(task, dirGlob.sync).map(globToTask(task)); + tasks.push(...newTask); + } + + const filter = getFilterSync(options); + + let matches = []; + for (const task of tasks) { + matches = arrayUnion(matches, fastGlob.sync(task.pattern, task.options)); + } + + return matches.filter(path_ => !filter(path_)); +}; + +module.exports.stream = (patterns, options) => { + const globTasks = generateGlobTasks(patterns, options); + + const tasks = []; + for (const task of globTasks) { + const newTask = getPattern(task, dirGlob.sync).map(globToTask(task)); + tasks.push(...newTask); + } + + const filter = getFilterSync(options); + const filterStream = new FilterStream(p => !filter(p)); + const uniqueStream = new UniqueStream(); + + return merge2(tasks.map(task => fastGlob.stream(task.pattern, task.options))) + .pipe(filterStream) + .pipe(uniqueStream); }; module.exports.generateGlobTasks = generateGlobTasks; -module.exports.hasMagic = (patterns, opts) => [] +module.exports.hasMagic = (patterns, options) => [] .concat(patterns) - .some(pattern => glob.hasMagic(pattern, opts)); + .some(pattern => fastGlob.isDynamicPattern(pattern, options)); module.exports.gitignore = gitignore; @@ -90398,12 +90493,73 @@ module.exports.gitignore = gitignore; /***/ (function(module, exports, __webpack_require__) { "use strict"; - -var arrayUniq = __webpack_require__(571); - -module.exports = function () { - return arrayUniq([].concat.apply([], arguments)); -}; + +const taskManager = __webpack_require__(776); +const async_1 = __webpack_require__(796); +const stream_1 = __webpack_require__(806); +const sync_1 = __webpack_require__(807); +const settings_1 = __webpack_require__(809); +const utils = __webpack_require__(777); +async function FastGlob(source, options) { + assertPatternsInput(source); + const works = getWorks(source, async_1.default, options); + const result = await Promise.all(works); + return utils.array.flatten(result); +} +// https://github.com/typescript-eslint/typescript-eslint/issues/60 +// eslint-disable-next-line no-redeclare +(function (FastGlob) { + function sync(source, options) { + assertPatternsInput(source); + const works = getWorks(source, sync_1.default, options); + return utils.array.flatten(works); + } + FastGlob.sync = sync; + function stream(source, options) { + assertPatternsInput(source); + const works = getWorks(source, stream_1.default, options); + /** + * The stream returned by the provider cannot work with an asynchronous iterator. + * To support asynchronous iterators, regardless of the number of tasks, we always multiplex streams. + * This affects performance (+25%). I don't see best solution right now. + */ + return utils.stream.merge(works); + } + FastGlob.stream = stream; + function generateTasks(source, options) { + assertPatternsInput(source); + const patterns = [].concat(source); + const settings = new settings_1.default(options); + return taskManager.generate(patterns, settings); + } + FastGlob.generateTasks = generateTasks; + function isDynamicPattern(source, options) { + assertPatternsInput(source); + const settings = new settings_1.default(options); + return utils.pattern.isDynamicPattern(source, settings); + } + FastGlob.isDynamicPattern = isDynamicPattern; + function escapePath(source) { + assertPatternsInput(source); + return utils.path.escape(source); + } + FastGlob.escapePath = escapePath; +})(FastGlob || (FastGlob = {})); +function getWorks(source, _Provider, options) { + const patterns = [].concat(source); + const settings = new settings_1.default(options); + const tasks = taskManager.generate(patterns, settings); + const provider = new _Provider(settings); + return tasks.map(provider.read, provider); +} +function assertPatternsInput(input) { + const source = [].concat(input); + const isValidSource = source.every((item) => utils.string.isString(item) && !utils.string.isEmpty(item)); + if (!isValidSource) { + throw new TypeError('Patterns must be a string (non empty) or an array of strings'); + } +} +module.exports = FastGlob; /***/ }), @@ -90411,54 +90567,71 @@ module.exports = function () { /***/ (function(module, exports, __webpack_require__) { "use strict"; - -const path = __webpack_require__(4); -const arrify = __webpack_require__(777); -const pathType = __webpack_require__(778); - -const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; -const getPath = filepath => filepath[0] === '!' ? filepath.slice(1) : filepath; - -const addExtensions = (file, extensions) => { - if (path.extname(file)) { - return `**/${file}`; - } - - return `**/${file}.${getExtensions(extensions)}`; -}; - -const getGlob = (dir, opts) => { - opts = Object.assign({}, opts); - - if (opts.files && !Array.isArray(opts.files)) { - throw new TypeError(`\`options.files\` must be an \`Array\`, not \`${typeof opts.files}\``); - } - - if (opts.extensions && !Array.isArray(opts.extensions)) { - throw new TypeError(`\`options.extensions\` must be an \`Array\`, not \`${typeof opts.extensions}\``); - } - - if (opts.files && opts.extensions) { - return opts.files.map(x => path.join(dir, addExtensions(x, opts.extensions))); - } else if (opts.files) { - return opts.files.map(x => path.join(dir, `**/${x}`)); - } else if (opts.extensions) { - return [path.join(dir, `**/*.${getExtensions(opts.extensions)}`)]; - } - - return [path.join(dir, '**')]; -}; - -module.exports = (input, opts) => { - return Promise.all(arrify(input).map(x => pathType.dir(getPath(x)) - .then(isDir => isDir ? getGlob(x, opts) : x))) - .then(globs => [].concat.apply([], globs)); -}; - -module.exports.sync = (input, opts) => { - const globs = arrify(input).map(x => pathType.dirSync(getPath(x)) ? getGlob(x, opts) : x); - return [].concat.apply([], globs); -}; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.convertPatternGroupToTask = exports.convertPatternGroupsToTasks = exports.groupPatternsByBaseDirectory = exports.getNegativePatternsAsPositive = exports.getPositivePatterns = exports.convertPatternsToTasks = exports.generate = void 0; +const utils = __webpack_require__(777); +function generate(patterns, settings) { + const positivePatterns = getPositivePatterns(patterns); + const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); + const staticPatterns = positivePatterns.filter((pattern) => utils.pattern.isStaticPattern(pattern, settings)); + const dynamicPatterns = positivePatterns.filter((pattern) => utils.pattern.isDynamicPattern(pattern, settings)); + const staticTasks = convertPatternsToTasks(staticPatterns, negativePatterns, /* dynamic */ false); + const dynamicTasks = convertPatternsToTasks(dynamicPatterns, negativePatterns, /* dynamic */ true); + return staticTasks.concat(dynamicTasks); +} +exports.generate = generate; +function convertPatternsToTasks(positive, negative, dynamic) { + const positivePatternsGroup = groupPatternsByBaseDirectory(positive); + // When we have a global group – there is no reason to divide the patterns into independent tasks. + // In this case, the global task covers the rest. + if ('.' in positivePatternsGroup) { + const task = convertPatternGroupToTask('.', positive, negative, dynamic); + return [task]; + } + return convertPatternGroupsToTasks(positivePatternsGroup, negative, dynamic); +} +exports.convertPatternsToTasks = convertPatternsToTasks; +function getPositivePatterns(patterns) { + return utils.pattern.getPositivePatterns(patterns); +} +exports.getPositivePatterns = getPositivePatterns; +function getNegativePatternsAsPositive(patterns, ignore) { + const negative = utils.pattern.getNegativePatterns(patterns).concat(ignore); + const positive = negative.map(utils.pattern.convertToPositivePattern); + return positive; +} +exports.getNegativePatternsAsPositive = getNegativePatternsAsPositive; +function groupPatternsByBaseDirectory(patterns) { + const group = {}; + return patterns.reduce((collection, pattern) => { + const base = utils.pattern.getBaseDirectory(pattern); + if (base in collection) { + collection[base].push(pattern); + } + else { + collection[base] = [pattern]; + } + return collection; + }, group); +} +exports.groupPatternsByBaseDirectory = groupPatternsByBaseDirectory; +function convertPatternGroupsToTasks(positive, negative, dynamic) { + return Object.keys(positive).map((base) => { + return convertPatternGroupToTask(base, positive[base], negative, dynamic); + }); +} +exports.convertPatternGroupsToTasks = convertPatternGroupsToTasks; +function convertPatternGroupToTask(base, positive, negative, dynamic) { + return { + dynamic, + positive, + negative, + base, + patterns: [].concat(positive, negative.map(utils.pattern.convertToNegativePattern)) + }; +} +exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), @@ -90466,14 +90639,23 @@ module.exports.sync = (input, opts) => { /***/ (function(module, exports, __webpack_require__) { "use strict"; - -module.exports = function (val) { - if (val === null || val === undefined) { - return []; - } - - return Array.isArray(val) ? val : [val]; -}; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.string = exports.stream = exports.pattern = exports.path = exports.fs = exports.errno = exports.array = void 0; +const array = __webpack_require__(778); +exports.array = array; +const errno = __webpack_require__(779); +exports.errno = errno; +const fs = __webpack_require__(780); +exports.fs = fs; +const path = __webpack_require__(781); +exports.path = path; +const pattern = __webpack_require__(782); +exports.pattern = pattern; +const stream = __webpack_require__(794); +exports.stream = stream; +const string = __webpack_require__(795); +exports.string = string; /***/ }), @@ -90481,204 +90663,3005 @@ module.exports = function (val) { /***/ (function(module, exports, __webpack_require__) { "use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.splitWhen = exports.flatten = void 0; +function flatten(items) { + return items.reduce((collection, item) => [].concat(collection, item), []); +} +exports.flatten = flatten; +function splitWhen(items, predicate) { + const result = [[]]; + let groupIndex = 0; + for (const item of items) { + if (predicate(item)) { + groupIndex++; + result[groupIndex] = []; + } + else { + result[groupIndex].push(item); + } + } + return result; +} +exports.splitWhen = splitWhen; -const fs = __webpack_require__(134); -const pify = __webpack_require__(779); - -function type(fn, fn2, fp) { - if (typeof fp !== 'string') { - return Promise.reject(new TypeError(`Expected a string, got ${typeof fp}`)); - } - - return pify(fs[fn])(fp) - .then(stats => stats[fn2]()) - .catch(err => { - if (err.code === 'ENOENT') { - return false; - } - - throw err; - }); -} - -function typeSync(fn, fn2, fp) { - if (typeof fp !== 'string') { - throw new TypeError(`Expected a string, got ${typeof fp}`); - } - - try { - return fs[fn](fp)[fn2](); - } catch (err) { - if (err.code === 'ENOENT') { - return false; - } - throw err; - } -} +/***/ }), +/* 779 */ +/***/ (function(module, exports, __webpack_require__) { -exports.file = type.bind(null, 'stat', 'isFile'); -exports.dir = type.bind(null, 'stat', 'isDirectory'); -exports.symlink = type.bind(null, 'lstat', 'isSymbolicLink'); -exports.fileSync = typeSync.bind(null, 'statSync', 'isFile'); -exports.dirSync = typeSync.bind(null, 'statSync', 'isDirectory'); -exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEnoentCodeError = void 0; +function isEnoentCodeError(error) { + return error.code === 'ENOENT'; +} +exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 779 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createDirentFromStats = void 0; +class DirentFromStats { + constructor(name, stats) { + this.name = name; + this.isBlockDevice = stats.isBlockDevice.bind(stats); + this.isCharacterDevice = stats.isCharacterDevice.bind(stats); + this.isDirectory = stats.isDirectory.bind(stats); + this.isFIFO = stats.isFIFO.bind(stats); + this.isFile = stats.isFile.bind(stats); + this.isSocket = stats.isSocket.bind(stats); + this.isSymbolicLink = stats.isSymbolicLink.bind(stats); + } +} +function createDirentFromStats(name, stats) { + return new DirentFromStats(name, stats); +} +exports.createDirentFromStats = createDirentFromStats; -const processFn = (fn, opts) => function () { - const P = opts.promiseModule; - const args = new Array(arguments.length); +/***/ }), +/* 781 */ +/***/ (function(module, exports, __webpack_require__) { - for (let i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; - } +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.removeLeadingDotSegment = exports.escape = exports.makeAbsolute = exports.unixify = void 0; +const path = __webpack_require__(4); +const LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2; // ./ or .\\ +const UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\())/g; +/** + * Designed to work only with simple paths: `dir\\file`. + */ +function unixify(filepath) { + return filepath.replace(/\\/g, '/'); +} +exports.unixify = unixify; +function makeAbsolute(cwd, filepath) { + return path.resolve(cwd, filepath); +} +exports.makeAbsolute = makeAbsolute; +function escape(pattern) { + return pattern.replace(UNESCAPED_GLOB_SYMBOLS_RE, '\\$2'); +} +exports.escape = escape; +function removeLeadingDotSegment(entry) { + // We do not use `startsWith` because this is 10x slower than current implementation for some cases. + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + if (entry.charAt(0) === '.') { + const secondCharactery = entry.charAt(1); + if (secondCharactery === '/' || secondCharactery === '\\') { + return entry.slice(LEADING_DOT_SEGMENT_CHARACTERS_COUNT); + } + } + return entry; +} +exports.removeLeadingDotSegment = removeLeadingDotSegment; - return new P((resolve, reject) => { - if (opts.errorFirst) { - args.push(function (err, result) { - if (opts.multiArgs) { - const results = new Array(arguments.length - 1); - for (let i = 1; i < arguments.length; i++) { - results[i - 1] = arguments[i]; - } +/***/ }), +/* 782 */ +/***/ (function(module, exports, __webpack_require__) { - if (err) { - results.unshift(err); - reject(results); - } else { - resolve(results); - } - } else if (err) { - reject(err); - } else { - resolve(result); - } - }); - } else { - args.push(function (result) { - if (opts.multiArgs) { - const results = new Array(arguments.length - 1); +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0; +const path = __webpack_require__(4); +const globParent = __webpack_require__(171); +const micromatch = __webpack_require__(783); +const picomatch = __webpack_require__(185); +const GLOBSTAR = '**'; +const ESCAPE_SYMBOL = '\\'; +const COMMON_GLOB_SYMBOLS_RE = /[*?]|^!/; +const REGEX_CHARACTER_CLASS_SYMBOLS_RE = /\[.*]/; +const REGEX_GROUP_SYMBOLS_RE = /(?:^|[^!*+?@])\(.*\|.*\)/; +const GLOB_EXTENSION_SYMBOLS_RE = /[!*+?@]\(.*\)/; +const BRACE_EXPANSIONS_SYMBOLS_RE = /{.*(?:,|\.\.).*}/; +function isStaticPattern(pattern, options = {}) { + return !isDynamicPattern(pattern, options); +} +exports.isStaticPattern = isStaticPattern; +function isDynamicPattern(pattern, options = {}) { + /** + * A special case with an empty string is necessary for matching patterns that start with a forward slash. + * An empty string cannot be a dynamic pattern. + * For example, the pattern `/lib/*` will be spread into parts: '', 'lib', '*'. + */ + if (pattern === '') { + return false; + } + /** + * When the `caseSensitiveMatch` option is disabled, all patterns must be marked as dynamic, because we cannot check + * filepath directly (without read directory). + */ + if (options.caseSensitiveMatch === false || pattern.includes(ESCAPE_SYMBOL)) { + return true; + } + if (COMMON_GLOB_SYMBOLS_RE.test(pattern) || REGEX_CHARACTER_CLASS_SYMBOLS_RE.test(pattern) || REGEX_GROUP_SYMBOLS_RE.test(pattern)) { + return true; + } + if (options.extglob !== false && GLOB_EXTENSION_SYMBOLS_RE.test(pattern)) { + return true; + } + if (options.braceExpansion !== false && BRACE_EXPANSIONS_SYMBOLS_RE.test(pattern)) { + return true; + } + return false; +} +exports.isDynamicPattern = isDynamicPattern; +function convertToPositivePattern(pattern) { + return isNegativePattern(pattern) ? pattern.slice(1) : pattern; +} +exports.convertToPositivePattern = convertToPositivePattern; +function convertToNegativePattern(pattern) { + return '!' + pattern; +} +exports.convertToNegativePattern = convertToNegativePattern; +function isNegativePattern(pattern) { + return pattern.startsWith('!') && pattern[1] !== '('; +} +exports.isNegativePattern = isNegativePattern; +function isPositivePattern(pattern) { + return !isNegativePattern(pattern); +} +exports.isPositivePattern = isPositivePattern; +function getNegativePatterns(patterns) { + return patterns.filter(isNegativePattern); +} +exports.getNegativePatterns = getNegativePatterns; +function getPositivePatterns(patterns) { + return patterns.filter(isPositivePattern); +} +exports.getPositivePatterns = getPositivePatterns; +function getBaseDirectory(pattern) { + return globParent(pattern, { flipBackslashes: false }); +} +exports.getBaseDirectory = getBaseDirectory; +function hasGlobStar(pattern) { + return pattern.includes(GLOBSTAR); +} +exports.hasGlobStar = hasGlobStar; +function endsWithSlashGlobStar(pattern) { + return pattern.endsWith('/' + GLOBSTAR); +} +exports.endsWithSlashGlobStar = endsWithSlashGlobStar; +function isAffectDepthOfReadingPattern(pattern) { + const basename = path.basename(pattern); + return endsWithSlashGlobStar(pattern) || isStaticPattern(basename); +} +exports.isAffectDepthOfReadingPattern = isAffectDepthOfReadingPattern; +function expandPatternsWithBraceExpansion(patterns) { + return patterns.reduce((collection, pattern) => { + return collection.concat(expandBraceExpansion(pattern)); + }, []); +} +exports.expandPatternsWithBraceExpansion = expandPatternsWithBraceExpansion; +function expandBraceExpansion(pattern) { + return micromatch.braces(pattern, { + expand: true, + nodupes: true + }); +} +exports.expandBraceExpansion = expandBraceExpansion; +function getPatternParts(pattern, options) { + let { parts } = picomatch.scan(pattern, Object.assign(Object.assign({}, options), { parts: true })); + /** + * The scan method returns an empty array in some cases. + * See micromatch/picomatch#58 for more details. + */ + if (parts.length === 0) { + parts = [pattern]; + } + /** + * The scan method does not return an empty part for the pattern with a forward slash. + * This is another part of micromatch/picomatch#58. + */ + if (parts[0].startsWith('/')) { + parts[0] = parts[0].slice(1); + parts.unshift(''); + } + return parts; +} +exports.getPatternParts = getPatternParts; +function makeRe(pattern, options) { + return micromatch.makeRe(pattern, options); +} +exports.makeRe = makeRe; +function convertPatternsToRe(patterns, options) { + return patterns.map((pattern) => makeRe(pattern, options)); +} +exports.convertPatternsToRe = convertPatternsToRe; +function matchAny(entry, patternsRe) { + return patternsRe.some((patternRe) => patternRe.test(entry)); +} +exports.matchAny = matchAny; + + +/***/ }), +/* 783 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const util = __webpack_require__(112); +const braces = __webpack_require__(784); +const picomatch = __webpack_require__(185); +const utils = __webpack_require__(188); +const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); + +/** + * Returns an array of strings that match one or more glob patterns. + * + * ```js + * const mm = require('micromatch'); + * // mm(list, patterns[, options]); + * + * console.log(mm(['a.js', 'a.txt'], ['*.js'])); + * //=> [ 'a.js' ] + * ``` + * @param {String|Array} list List of strings to match. + * @param {String|Array} patterns One or more glob patterns to use for matching. + * @param {Object} options See available [options](#options) + * @return {Array} Returns an array of matches + * @summary false + * @api public + */ + +const micromatch = (list, patterns, options) => { + patterns = [].concat(patterns); + list = [].concat(list); + + let omit = new Set(); + let keep = new Set(); + let items = new Set(); + let negatives = 0; + + let onResult = state => { + items.add(state.output); + if (options && options.onResult) { + options.onResult(state); + } + }; + + for (let i = 0; i < patterns.length; i++) { + let isMatch = picomatch(String(patterns[i]), { ...options, onResult }, true); + let negated = isMatch.state.negated || isMatch.state.negatedExtglob; + if (negated) negatives++; + + for (let item of list) { + let matched = isMatch(item, true); + + let match = negated ? !matched.isMatch : matched.isMatch; + if (!match) continue; + + if (negated) { + omit.add(matched.output); + } else { + omit.delete(matched.output); + keep.add(matched.output); + } + } + } + + let result = negatives === patterns.length ? [...items] : [...keep]; + let matches = result.filter(item => !omit.has(item)); + + if (options && matches.length === 0) { + if (options.failglob === true) { + throw new Error(`No matches found for "${patterns.join(', ')}"`); + } + + if (options.nonull === true || options.nullglob === true) { + return options.unescape ? patterns.map(p => p.replace(/\\/g, '')) : patterns; + } + } + + return matches; +}; + +/** + * Backwards compatibility + */ + +micromatch.match = micromatch; + +/** + * Returns a matcher function from the given glob `pattern` and `options`. + * The returned function takes a string to match as its only argument and returns + * true if the string is a match. + * + * ```js + * const mm = require('micromatch'); + * // mm.matcher(pattern[, options]); + * + * const isMatch = mm.matcher('*.!(*a)'); + * console.log(isMatch('a.a')); //=> false + * console.log(isMatch('a.b')); //=> true + * ``` + * @param {String} `pattern` Glob pattern + * @param {Object} `options` + * @return {Function} Returns a matcher function. + * @api public + */ + +micromatch.matcher = (pattern, options) => picomatch(pattern, options); + +/** + * Returns true if **any** of the given glob `patterns` match the specified `string`. + * + * ```js + * const mm = require('micromatch'); + * // mm.isMatch(string, patterns[, options]); + * + * console.log(mm.isMatch('a.a', ['b.*', '*.a'])); //=> true + * console.log(mm.isMatch('a.a', 'b.*')); //=> false + * ``` + * @param {String} str The string to test. + * @param {String|Array} patterns One or more glob patterns to use for matching. + * @param {Object} [options] See available [options](#options). + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.isMatch = (str, patterns, options) => picomatch(patterns, options)(str); + +/** + * Backwards compatibility + */ + +micromatch.any = micromatch.isMatch; + +/** + * Returns a list of strings that _**do not match any**_ of the given `patterns`. + * + * ```js + * const mm = require('micromatch'); + * // mm.not(list, patterns[, options]); + * + * console.log(mm.not(['a.a', 'b.b', 'c.c'], '*.a')); + * //=> ['b.b', 'c.c'] + * ``` + * @param {Array} `list` Array of strings to match. + * @param {String|Array} `patterns` One or more glob pattern to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Array} Returns an array of strings that **do not match** the given patterns. + * @api public + */ + +micromatch.not = (list, patterns, options = {}) => { + patterns = [].concat(patterns).map(String); + let result = new Set(); + let items = []; + + let onResult = state => { + if (options.onResult) options.onResult(state); + items.push(state.output); + }; + + let matches = micromatch(list, patterns, { ...options, onResult }); + + for (let item of items) { + if (!matches.includes(item)) { + result.add(item); + } + } + return [...result]; +}; + +/** + * Returns true if the given `string` contains the given pattern. Similar + * to [.isMatch](#isMatch) but the pattern can match any part of the string. + * + * ```js + * var mm = require('micromatch'); + * // mm.contains(string, pattern[, options]); + * + * console.log(mm.contains('aa/bb/cc', '*b')); + * //=> true + * console.log(mm.contains('aa/bb/cc', '*d')); + * //=> false + * ``` + * @param {String} `str` The string to match. + * @param {String|Array} `patterns` Glob pattern to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if the patter matches any part of `str`. + * @api public + */ + +micromatch.contains = (str, pattern, options) => { + if (typeof str !== 'string') { + throw new TypeError(`Expected a string: "${util.inspect(str)}"`); + } + + if (Array.isArray(pattern)) { + return pattern.some(p => micromatch.contains(str, p, options)); + } + + if (typeof pattern === 'string') { + if (isEmptyString(str) || isEmptyString(pattern)) { + return false; + } + + if (str.includes(pattern) || (str.startsWith('./') && str.slice(2).includes(pattern))) { + return true; + } + } + + return micromatch.isMatch(str, pattern, { ...options, contains: true }); +}; + +/** + * Filter the keys of the given object with the given `glob` pattern + * and `options`. Does not attempt to match nested keys. If you need this feature, + * use [glob-object][] instead. + * + * ```js + * const mm = require('micromatch'); + * // mm.matchKeys(object, patterns[, options]); + * + * const obj = { aa: 'a', ab: 'b', ac: 'c' }; + * console.log(mm.matchKeys(obj, '*b')); + * //=> { ab: 'b' } + * ``` + * @param {Object} `object` The object with keys to filter. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Object} Returns an object with only keys that match the given patterns. + * @api public + */ + +micromatch.matchKeys = (obj, patterns, options) => { + if (!utils.isObject(obj)) { + throw new TypeError('Expected the first argument to be an object'); + } + let keys = micromatch(Object.keys(obj), patterns, options); + let res = {}; + for (let key of keys) res[key] = obj[key]; + return res; +}; + +/** + * Returns true if some of the strings in the given `list` match any of the given glob `patterns`. + * + * ```js + * const mm = require('micromatch'); + * // mm.some(list, patterns[, options]); + * + * console.log(mm.some(['foo.js', 'bar.js'], ['*.js', '!foo.js'])); + * // true + * console.log(mm.some(['foo.js'], ['*.js', '!foo.js'])); + * // false + * ``` + * @param {String|Array} `list` The string or array of strings to test. Returns as soon as the first match is found. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.some = (list, patterns, options) => { + let items = [].concat(list); + + for (let pattern of [].concat(patterns)) { + let isMatch = picomatch(String(pattern), options); + if (items.some(item => isMatch(item))) { + return true; + } + } + return false; +}; + +/** + * Returns true if every string in the given `list` matches + * any of the given glob `patterns`. + * + * ```js + * const mm = require('micromatch'); + * // mm.every(list, patterns[, options]); + * + * console.log(mm.every('foo.js', ['foo.js'])); + * // true + * console.log(mm.every(['foo.js', 'bar.js'], ['*.js'])); + * // true + * console.log(mm.every(['foo.js', 'bar.js'], ['*.js', '!foo.js'])); + * // false + * console.log(mm.every(['foo.js'], ['*.js', '!foo.js'])); + * // false + * ``` + * @param {String|Array} `list` The string or array of strings to test. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.every = (list, patterns, options) => { + let items = [].concat(list); + + for (let pattern of [].concat(patterns)) { + let isMatch = picomatch(String(pattern), options); + if (!items.every(item => isMatch(item))) { + return false; + } + } + return true; +}; + +/** + * Returns true if **all** of the given `patterns` match + * the specified string. + * + * ```js + * const mm = require('micromatch'); + * // mm.all(string, patterns[, options]); + * + * console.log(mm.all('foo.js', ['foo.js'])); + * // true + * + * console.log(mm.all('foo.js', ['*.js', '!foo.js'])); + * // false + * + * console.log(mm.all('foo.js', ['*.js', 'foo.js'])); + * // true + * + * console.log(mm.all('foo.js', ['*.js', 'f*', '*o*', '*o.js'])); + * // true + * ``` + * @param {String|Array} `str` The string to test. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.all = (str, patterns, options) => { + if (typeof str !== 'string') { + throw new TypeError(`Expected a string: "${util.inspect(str)}"`); + } + + return [].concat(patterns).every(p => picomatch(p, options)(str)); +}; + +/** + * Returns an array of matches captured by `pattern` in `string, or `null` if the pattern did not match. + * + * ```js + * const mm = require('micromatch'); + * // mm.capture(pattern, string[, options]); + * + * console.log(mm.capture('test/*.js', 'test/foo.js')); + * //=> ['foo'] + * console.log(mm.capture('test/*.js', 'foo/bar.css')); + * //=> null + * ``` + * @param {String} `glob` Glob pattern to use for matching. + * @param {String} `input` String to match + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns an array of captures if the input matches the glob pattern, otherwise `null`. + * @api public + */ + +micromatch.capture = (glob, input, options) => { + let posix = utils.isWindows(options); + let regex = picomatch.makeRe(String(glob), { ...options, capture: true }); + let match = regex.exec(posix ? utils.toPosixSlashes(input) : input); + + if (match) { + return match.slice(1).map(v => v === void 0 ? '' : v); + } +}; + +/** + * Create a regular expression from the given glob `pattern`. + * + * ```js + * const mm = require('micromatch'); + * // mm.makeRe(pattern[, options]); + * + * console.log(mm.makeRe('*.js')); + * //=> /^(?:(\.[\\\/])?(?!\.)(?=.)[^\/]*?\.js)$/ + * ``` + * @param {String} `pattern` A glob pattern to convert to regex. + * @param {Object} `options` + * @return {RegExp} Returns a regex created from the given pattern. + * @api public + */ + +micromatch.makeRe = (...args) => picomatch.makeRe(...args); + +/** + * Scan a glob pattern to separate the pattern into segments. Used + * by the [split](#split) method. + * + * ```js + * const mm = require('micromatch'); + * const state = mm.scan(pattern[, options]); + * ``` + * @param {String} `pattern` + * @param {Object} `options` + * @return {Object} Returns an object with + * @api public + */ + +micromatch.scan = (...args) => picomatch.scan(...args); + +/** + * Parse a glob pattern to create the source string for a regular + * expression. + * + * ```js + * const mm = require('micromatch'); + * const state = mm(pattern[, options]); + * ``` + * @param {String} `glob` + * @param {Object} `options` + * @return {Object} Returns an object with useful properties and output to be used as regex source string. + * @api public + */ + +micromatch.parse = (patterns, options) => { + let res = []; + for (let pattern of [].concat(patterns || [])) { + for (let str of braces(String(pattern), options)) { + res.push(picomatch.parse(str, options)); + } + } + return res; +}; + +/** + * Process the given brace `pattern`. + * + * ```js + * const { braces } = require('micromatch'); + * console.log(braces('foo/{a,b,c}/bar')); + * //=> [ 'foo/(a|b|c)/bar' ] + * + * console.log(braces('foo/{a,b,c}/bar', { expand: true })); + * //=> [ 'foo/a/bar', 'foo/b/bar', 'foo/c/bar' ] + * ``` + * @param {String} `pattern` String with brace pattern to process. + * @param {Object} `options` Any [options](#options) to change how expansion is performed. See the [braces][] library for all available options. + * @return {Array} + * @api public + */ + +micromatch.braces = (pattern, options) => { + if (typeof pattern !== 'string') throw new TypeError('Expected a string'); + if ((options && options.nobrace === true) || !/\{.*\}/.test(pattern)) { + return [pattern]; + } + return braces(pattern, options); +}; + +/** + * Expand braces + */ + +micromatch.braceExpand = (pattern, options) => { + if (typeof pattern !== 'string') throw new TypeError('Expected a string'); + return micromatch.braces(pattern, { ...options, expand: true }); +}; + +/** + * Expose micromatch + */ + +module.exports = micromatch; + + +/***/ }), +/* 784 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const stringify = __webpack_require__(785); +const compile = __webpack_require__(787); +const expand = __webpack_require__(791); +const parse = __webpack_require__(792); + +/** + * Expand the given pattern or create a regex-compatible string. + * + * ```js + * const braces = require('braces'); + * console.log(braces('{a,b,c}', { compile: true })); //=> ['(a|b|c)'] + * console.log(braces('{a,b,c}')); //=> ['a', 'b', 'c'] + * ``` + * @param {String} `str` + * @param {Object} `options` + * @return {String} + * @api public + */ + +const braces = (input, options = {}) => { + let output = []; + + if (Array.isArray(input)) { + for (let pattern of input) { + let result = braces.create(pattern, options); + if (Array.isArray(result)) { + output.push(...result); + } else { + output.push(result); + } + } + } else { + output = [].concat(braces.create(input, options)); + } + + if (options && options.expand === true && options.nodupes === true) { + output = [...new Set(output)]; + } + return output; +}; + +/** + * Parse the given `str` with the given `options`. + * + * ```js + * // braces.parse(pattern, [, options]); + * const ast = braces.parse('a/{b,c}/d'); + * console.log(ast); + * ``` + * @param {String} pattern Brace pattern to parse + * @param {Object} options + * @return {Object} Returns an AST + * @api public + */ + +braces.parse = (input, options = {}) => parse(input, options); + +/** + * Creates a braces string from an AST, or an AST node. + * + * ```js + * const braces = require('braces'); + * let ast = braces.parse('foo/{a,b}/bar'); + * console.log(stringify(ast.nodes[2])); //=> '{a,b}' + * ``` + * @param {String} `input` Brace pattern or AST. + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.stringify = (input, options = {}) => { + if (typeof input === 'string') { + return stringify(braces.parse(input, options), options); + } + return stringify(input, options); +}; + +/** + * Compiles a brace pattern into a regex-compatible, optimized string. + * This method is called by the main [braces](#braces) function by default. + * + * ```js + * const braces = require('braces'); + * console.log(braces.compile('a/{b,c}/d')); + * //=> ['a/(b|c)/d'] + * ``` + * @param {String} `input` Brace pattern or AST. + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.compile = (input, options = {}) => { + if (typeof input === 'string') { + input = braces.parse(input, options); + } + return compile(input, options); +}; + +/** + * Expands a brace pattern into an array. This method is called by the + * main [braces](#braces) function when `options.expand` is true. Before + * using this method it's recommended that you read the [performance notes](#performance)) + * and advantages of using [.compile](#compile) instead. + * + * ```js + * const braces = require('braces'); + * console.log(braces.expand('a/{b,c}/d')); + * //=> ['a/b/d', 'a/c/d']; + * ``` + * @param {String} `pattern` Brace pattern + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.expand = (input, options = {}) => { + if (typeof input === 'string') { + input = braces.parse(input, options); + } + + let result = expand(input, options); + + // filter out empty strings if specified + if (options.noempty === true) { + result = result.filter(Boolean); + } + + // filter out duplicates if specified + if (options.nodupes === true) { + result = [...new Set(result)]; + } + + return result; +}; + +/** + * Processes a brace pattern and returns either an expanded array + * (if `options.expand` is true), a highly optimized regex-compatible string. + * This method is called by the main [braces](#braces) function. + * + * ```js + * const braces = require('braces'); + * console.log(braces.create('user-{200..300}/project-{a,b,c}-{1..10}')) + * //=> 'user-(20[0-9]|2[1-9][0-9]|300)/project-(a|b|c)-([1-9]|10)' + * ``` + * @param {String} `pattern` Brace pattern + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.create = (input, options = {}) => { + if (input === '' || input.length < 3) { + return [input]; + } + + return options.expand !== true + ? braces.compile(input, options) + : braces.expand(input, options); +}; + +/** + * Expose "braces" + */ + +module.exports = braces; + + +/***/ }), +/* 785 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const utils = __webpack_require__(786); + +module.exports = (ast, options = {}) => { + let stringify = (node, parent = {}) => { + let invalidBlock = options.escapeInvalid && utils.isInvalidBrace(parent); + let invalidNode = node.invalid === true && options.escapeInvalid === true; + let output = ''; + + if (node.value) { + if ((invalidBlock || invalidNode) && utils.isOpenOrClose(node)) { + return '\\' + node.value; + } + return node.value; + } + + if (node.value) { + return node.value; + } + + if (node.nodes) { + for (let child of node.nodes) { + output += stringify(child); + } + } + return output; + }; + + return stringify(ast); +}; + + + +/***/ }), +/* 786 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +exports.isInteger = num => { + if (typeof num === 'number') { + return Number.isInteger(num); + } + if (typeof num === 'string' && num.trim() !== '') { + return Number.isInteger(Number(num)); + } + return false; +}; + +/** + * Find a node of the given type + */ + +exports.find = (node, type) => node.nodes.find(node => node.type === type); + +/** + * Find a node of the given type + */ + +exports.exceedsLimit = (min, max, step = 1, limit) => { + if (limit === false) return false; + if (!exports.isInteger(min) || !exports.isInteger(max)) return false; + return ((Number(max) - Number(min)) / Number(step)) >= limit; +}; + +/** + * Escape the given node with '\\' before node.value + */ + +exports.escapeNode = (block, n = 0, type) => { + let node = block.nodes[n]; + if (!node) return; + + if ((type && node.type === type) || node.type === 'open' || node.type === 'close') { + if (node.escaped !== true) { + node.value = '\\' + node.value; + node.escaped = true; + } + } +}; + +/** + * Returns true if the given brace node should be enclosed in literal braces + */ + +exports.encloseBrace = node => { + if (node.type !== 'brace') return false; + if ((node.commas >> 0 + node.ranges >> 0) === 0) { + node.invalid = true; + return true; + } + return false; +}; + +/** + * Returns true if a brace node is invalid. + */ + +exports.isInvalidBrace = block => { + if (block.type !== 'brace') return false; + if (block.invalid === true || block.dollar) return true; + if ((block.commas >> 0 + block.ranges >> 0) === 0) { + block.invalid = true; + return true; + } + if (block.open !== true || block.close !== true) { + block.invalid = true; + return true; + } + return false; +}; + +/** + * Returns true if a node is an open or close node + */ + +exports.isOpenOrClose = node => { + if (node.type === 'open' || node.type === 'close') { + return true; + } + return node.open === true || node.close === true; +}; + +/** + * Reduce an array of text nodes. + */ + +exports.reduce = nodes => nodes.reduce((acc, node) => { + if (node.type === 'text') acc.push(node.value); + if (node.type === 'range') node.type = 'text'; + return acc; +}, []); + +/** + * Flatten an array + */ + +exports.flatten = (...args) => { + const result = []; + const flat = arr => { + for (let i = 0; i < arr.length; i++) { + let ele = arr[i]; + Array.isArray(ele) ? flat(ele, result) : ele !== void 0 && result.push(ele); + } + return result; + }; + flat(args); + return result; +}; + + +/***/ }), +/* 787 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const fill = __webpack_require__(788); +const utils = __webpack_require__(786); + +const compile = (ast, options = {}) => { + let walk = (node, parent = {}) => { + let invalidBlock = utils.isInvalidBrace(parent); + let invalidNode = node.invalid === true && options.escapeInvalid === true; + let invalid = invalidBlock === true || invalidNode === true; + let prefix = options.escapeInvalid === true ? '\\' : ''; + let output = ''; + + if (node.isOpen === true) { + return prefix + node.value; + } + if (node.isClose === true) { + return prefix + node.value; + } + + if (node.type === 'open') { + return invalid ? (prefix + node.value) : '('; + } + + if (node.type === 'close') { + return invalid ? (prefix + node.value) : ')'; + } + + if (node.type === 'comma') { + return node.prev.type === 'comma' ? '' : (invalid ? node.value : '|'); + } + + if (node.value) { + return node.value; + } + + if (node.nodes && node.ranges > 0) { + let args = utils.reduce(node.nodes); + let range = fill(...args, { ...options, wrap: false, toRegex: true }); + + if (range.length !== 0) { + return args.length > 1 && range.length > 1 ? `(${range})` : range; + } + } + + if (node.nodes) { + for (let child of node.nodes) { + output += walk(child, node); + } + } + return output; + }; + + return walk(ast); +}; + +module.exports = compile; + + +/***/ }), +/* 788 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +const util = __webpack_require__(112); +const toRegexRange = __webpack_require__(789); + +const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); + +const transform = toNumber => { + return value => toNumber === true ? Number(value) : String(value); +}; + +const isValidValue = value => { + return typeof value === 'number' || (typeof value === 'string' && value !== ''); +}; + +const isNumber = num => Number.isInteger(+num); + +const zeros = input => { + let value = `${input}`; + let index = -1; + if (value[0] === '-') value = value.slice(1); + if (value === '0') return false; + while (value[++index] === '0'); + return index > 0; +}; + +const stringify = (start, end, options) => { + if (typeof start === 'string' || typeof end === 'string') { + return true; + } + return options.stringify === true; +}; + +const pad = (input, maxLength, toNumber) => { + if (maxLength > 0) { + let dash = input[0] === '-' ? '-' : ''; + if (dash) input = input.slice(1); + input = (dash + input.padStart(dash ? maxLength - 1 : maxLength, '0')); + } + if (toNumber === false) { + return String(input); + } + return input; +}; + +const toMaxLen = (input, maxLength) => { + let negative = input[0] === '-' ? '-' : ''; + if (negative) { + input = input.slice(1); + maxLength--; + } + while (input.length < maxLength) input = '0' + input; + return negative ? ('-' + input) : input; +}; + +const toSequence = (parts, options) => { + parts.negatives.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); + parts.positives.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); + + let prefix = options.capture ? '' : '?:'; + let positives = ''; + let negatives = ''; + let result; + + if (parts.positives.length) { + positives = parts.positives.join('|'); + } + + if (parts.negatives.length) { + negatives = `-(${prefix}${parts.negatives.join('|')})`; + } + + if (positives && negatives) { + result = `${positives}|${negatives}`; + } else { + result = positives || negatives; + } + + if (options.wrap) { + return `(${prefix}${result})`; + } + + return result; +}; + +const toRange = (a, b, isNumbers, options) => { + if (isNumbers) { + return toRegexRange(a, b, { wrap: false, ...options }); + } + + let start = String.fromCharCode(a); + if (a === b) return start; + + let stop = String.fromCharCode(b); + return `[${start}-${stop}]`; +}; + +const toRegex = (start, end, options) => { + if (Array.isArray(start)) { + let wrap = options.wrap === true; + let prefix = options.capture ? '' : '?:'; + return wrap ? `(${prefix}${start.join('|')})` : start.join('|'); + } + return toRegexRange(start, end, options); +}; + +const rangeError = (...args) => { + return new RangeError('Invalid range arguments: ' + util.inspect(...args)); +}; + +const invalidRange = (start, end, options) => { + if (options.strictRanges === true) throw rangeError([start, end]); + return []; +}; + +const invalidStep = (step, options) => { + if (options.strictRanges === true) { + throw new TypeError(`Expected step "${step}" to be a number`); + } + return []; +}; + +const fillNumbers = (start, end, step = 1, options = {}) => { + let a = Number(start); + let b = Number(end); + + if (!Number.isInteger(a) || !Number.isInteger(b)) { + if (options.strictRanges === true) throw rangeError([start, end]); + return []; + } + + // fix negative zero + if (a === 0) a = 0; + if (b === 0) b = 0; + + let descending = a > b; + let startString = String(start); + let endString = String(end); + let stepString = String(step); + step = Math.max(Math.abs(step), 1); + + let padded = zeros(startString) || zeros(endString) || zeros(stepString); + let maxLen = padded ? Math.max(startString.length, endString.length, stepString.length) : 0; + let toNumber = padded === false && stringify(start, end, options) === false; + let format = options.transform || transform(toNumber); + + if (options.toRegex && step === 1) { + return toRange(toMaxLen(start, maxLen), toMaxLen(end, maxLen), true, options); + } + + let parts = { negatives: [], positives: [] }; + let push = num => parts[num < 0 ? 'negatives' : 'positives'].push(Math.abs(num)); + let range = []; + let index = 0; + + while (descending ? a >= b : a <= b) { + if (options.toRegex === true && step > 1) { + push(a); + } else { + range.push(pad(format(a, index), maxLen, toNumber)); + } + a = descending ? a - step : a + step; + index++; + } + + if (options.toRegex === true) { + return step > 1 + ? toSequence(parts, options) + : toRegex(range, null, { wrap: false, ...options }); + } + + return range; +}; + +const fillLetters = (start, end, step = 1, options = {}) => { + if ((!isNumber(start) && start.length > 1) || (!isNumber(end) && end.length > 1)) { + return invalidRange(start, end, options); + } + + + let format = options.transform || (val => String.fromCharCode(val)); + let a = `${start}`.charCodeAt(0); + let b = `${end}`.charCodeAt(0); + + let descending = a > b; + let min = Math.min(a, b); + let max = Math.max(a, b); + + if (options.toRegex && step === 1) { + return toRange(min, max, false, options); + } + + let range = []; + let index = 0; + + while (descending ? a >= b : a <= b) { + range.push(format(a, index)); + a = descending ? a - step : a + step; + index++; + } + + if (options.toRegex === true) { + return toRegex(range, null, { wrap: false, options }); + } + + return range; +}; + +const fill = (start, end, step, options = {}) => { + if (end == null && isValidValue(start)) { + return [start]; + } + + if (!isValidValue(start) || !isValidValue(end)) { + return invalidRange(start, end, options); + } + + if (typeof step === 'function') { + return fill(start, end, 1, { transform: step }); + } + + if (isObject(step)) { + return fill(start, end, 0, step); + } + + let opts = { ...options }; + if (opts.capture === true) opts.wrap = true; + step = step || opts.step || 1; + + if (!isNumber(step)) { + if (step != null && !isObject(step)) return invalidStep(step, opts); + return fill(start, end, 1, step); + } + + if (isNumber(start) && isNumber(end)) { + return fillNumbers(start, end, step, opts); + } + + return fillLetters(start, end, Math.max(Math.abs(step), 1), opts); +}; + +module.exports = fill; + + +/***/ }), +/* 789 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */ + + + +const isNumber = __webpack_require__(790); + +const toRegexRange = (min, max, options) => { + if (isNumber(min) === false) { + throw new TypeError('toRegexRange: expected the first argument to be a number'); + } + + if (max === void 0 || min === max) { + return String(min); + } + + if (isNumber(max) === false) { + throw new TypeError('toRegexRange: expected the second argument to be a number.'); + } + + let opts = { relaxZeros: true, ...options }; + if (typeof opts.strictZeros === 'boolean') { + opts.relaxZeros = opts.strictZeros === false; + } + + let relax = String(opts.relaxZeros); + let shorthand = String(opts.shorthand); + let capture = String(opts.capture); + let wrap = String(opts.wrap); + let cacheKey = min + ':' + max + '=' + relax + shorthand + capture + wrap; + + if (toRegexRange.cache.hasOwnProperty(cacheKey)) { + return toRegexRange.cache[cacheKey].result; + } + + let a = Math.min(min, max); + let b = Math.max(min, max); + + if (Math.abs(a - b) === 1) { + let result = min + '|' + max; + if (opts.capture) { + return `(${result})`; + } + if (opts.wrap === false) { + return result; + } + return `(?:${result})`; + } + + let isPadded = hasPadding(min) || hasPadding(max); + let state = { min, max, a, b }; + let positives = []; + let negatives = []; + + if (isPadded) { + state.isPadded = isPadded; + state.maxLen = String(state.max).length; + } + + if (a < 0) { + let newMin = b < 0 ? Math.abs(b) : 1; + negatives = splitToPatterns(newMin, Math.abs(a), state, opts); + a = state.a = 0; + } + + if (b >= 0) { + positives = splitToPatterns(a, b, state, opts); + } + + state.negatives = negatives; + state.positives = positives; + state.result = collatePatterns(negatives, positives, opts); + + if (opts.capture === true) { + state.result = `(${state.result})`; + } else if (opts.wrap !== false && (positives.length + negatives.length) > 1) { + state.result = `(?:${state.result})`; + } + + toRegexRange.cache[cacheKey] = state; + return state.result; +}; + +function collatePatterns(neg, pos, options) { + let onlyNegative = filterPatterns(neg, pos, '-', false, options) || []; + let onlyPositive = filterPatterns(pos, neg, '', false, options) || []; + let intersected = filterPatterns(neg, pos, '-?', true, options) || []; + let subpatterns = onlyNegative.concat(intersected).concat(onlyPositive); + return subpatterns.join('|'); +} + +function splitToRanges(min, max) { + let nines = 1; + let zeros = 1; + + let stop = countNines(min, nines); + let stops = new Set([max]); + + while (min <= stop && stop <= max) { + stops.add(stop); + nines += 1; + stop = countNines(min, nines); + } + + stop = countZeros(max + 1, zeros) - 1; + + while (min < stop && stop <= max) { + stops.add(stop); + zeros += 1; + stop = countZeros(max + 1, zeros) - 1; + } + + stops = [...stops]; + stops.sort(compare); + return stops; +} + +/** + * Convert a range to a regex pattern + * @param {Number} `start` + * @param {Number} `stop` + * @return {String} + */ + +function rangeToPattern(start, stop, options) { + if (start === stop) { + return { pattern: start, count: [], digits: 0 }; + } + + let zipped = zip(start, stop); + let digits = zipped.length; + let pattern = ''; + let count = 0; + + for (let i = 0; i < digits; i++) { + let [startDigit, stopDigit] = zipped[i]; + + if (startDigit === stopDigit) { + pattern += startDigit; + + } else if (startDigit !== '0' || stopDigit !== '9') { + pattern += toCharacterClass(startDigit, stopDigit, options); + + } else { + count++; + } + } + + if (count) { + pattern += options.shorthand === true ? '\\d' : '[0-9]'; + } + + return { pattern, count: [count], digits }; +} + +function splitToPatterns(min, max, tok, options) { + let ranges = splitToRanges(min, max); + let tokens = []; + let start = min; + let prev; + + for (let i = 0; i < ranges.length; i++) { + let max = ranges[i]; + let obj = rangeToPattern(String(start), String(max), options); + let zeros = ''; + + if (!tok.isPadded && prev && prev.pattern === obj.pattern) { + if (prev.count.length > 1) { + prev.count.pop(); + } + + prev.count.push(obj.count[0]); + prev.string = prev.pattern + toQuantifier(prev.count); + start = max + 1; + continue; + } + + if (tok.isPadded) { + zeros = padZeros(max, tok, options); + } + + obj.string = zeros + obj.pattern + toQuantifier(obj.count); + tokens.push(obj); + start = max + 1; + prev = obj; + } + + return tokens; +} + +function filterPatterns(arr, comparison, prefix, intersection, options) { + let result = []; + + for (let ele of arr) { + let { string } = ele; + + // only push if _both_ are negative... + if (!intersection && !contains(comparison, 'string', string)) { + result.push(prefix + string); + } + + // or _both_ are positive + if (intersection && contains(comparison, 'string', string)) { + result.push(prefix + string); + } + } + return result; +} + +/** + * Zip strings + */ + +function zip(a, b) { + let arr = []; + for (let i = 0; i < a.length; i++) arr.push([a[i], b[i]]); + return arr; +} + +function compare(a, b) { + return a > b ? 1 : b > a ? -1 : 0; +} + +function contains(arr, key, val) { + return arr.some(ele => ele[key] === val); +} + +function countNines(min, len) { + return Number(String(min).slice(0, -len) + '9'.repeat(len)); +} + +function countZeros(integer, zeros) { + return integer - (integer % Math.pow(10, zeros)); +} + +function toQuantifier(digits) { + let [start = 0, stop = ''] = digits; + if (stop || start > 1) { + return `{${start + (stop ? ',' + stop : '')}}`; + } + return ''; +} + +function toCharacterClass(a, b, options) { + return `[${a}${(b - a === 1) ? '' : '-'}${b}]`; +} + +function hasPadding(str) { + return /^-?(0+)\d/.test(str); +} + +function padZeros(value, tok, options) { + if (!tok.isPadded) { + return value; + } + + let diff = Math.abs(tok.maxLen - String(value).length); + let relax = options.relaxZeros !== false; + + switch (diff) { + case 0: + return ''; + case 1: + return relax ? '0?' : '0'; + case 2: + return relax ? '0{0,2}' : '00'; + default: { + return relax ? `0{0,${diff}}` : `0{${diff}}`; + } + } +} + +/** + * Cache + */ + +toRegexRange.cache = {}; +toRegexRange.clearCache = () => (toRegexRange.cache = {}); + +/** + * Expose `toRegexRange` + */ + +module.exports = toRegexRange; + + +/***/ }), +/* 790 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */ + + + +module.exports = function(num) { + if (typeof num === 'number') { + return num - num === 0; + } + if (typeof num === 'string' && num.trim() !== '') { + return Number.isFinite ? Number.isFinite(+num) : isFinite(+num); + } + return false; +}; + + +/***/ }), +/* 791 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const fill = __webpack_require__(788); +const stringify = __webpack_require__(785); +const utils = __webpack_require__(786); + +const append = (queue = '', stash = '', enclose = false) => { + let result = []; + + queue = [].concat(queue); + stash = [].concat(stash); + + if (!stash.length) return queue; + if (!queue.length) { + return enclose ? utils.flatten(stash).map(ele => `{${ele}}`) : stash; + } + + for (let item of queue) { + if (Array.isArray(item)) { + for (let value of item) { + result.push(append(value, stash, enclose)); + } + } else { + for (let ele of stash) { + if (enclose === true && typeof ele === 'string') ele = `{${ele}}`; + result.push(Array.isArray(ele) ? append(item, ele, enclose) : (item + ele)); + } + } + } + return utils.flatten(result); +}; + +const expand = (ast, options = {}) => { + let rangeLimit = options.rangeLimit === void 0 ? 1000 : options.rangeLimit; + + let walk = (node, parent = {}) => { + node.queue = []; + + let p = parent; + let q = parent.queue; + + while (p.type !== 'brace' && p.type !== 'root' && p.parent) { + p = p.parent; + q = p.queue; + } + + if (node.invalid || node.dollar) { + q.push(append(q.pop(), stringify(node, options))); + return; + } + + if (node.type === 'brace' && node.invalid !== true && node.nodes.length === 2) { + q.push(append(q.pop(), ['{}'])); + return; + } + + if (node.nodes && node.ranges > 0) { + let args = utils.reduce(node.nodes); + + if (utils.exceedsLimit(...args, options.step, rangeLimit)) { + throw new RangeError('expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.'); + } + + let range = fill(...args, options); + if (range.length === 0) { + range = stringify(node, options); + } + + q.push(append(q.pop(), range)); + node.nodes = []; + return; + } + + let enclose = utils.encloseBrace(node); + let queue = node.queue; + let block = node; + + while (block.type !== 'brace' && block.type !== 'root' && block.parent) { + block = block.parent; + queue = block.queue; + } + + for (let i = 0; i < node.nodes.length; i++) { + let child = node.nodes[i]; + + if (child.type === 'comma' && node.type === 'brace') { + if (i === 1) queue.push(''); + queue.push(''); + continue; + } + + if (child.type === 'close') { + q.push(append(q.pop(), queue, enclose)); + continue; + } + + if (child.value && child.type !== 'open') { + queue.push(append(queue.pop(), child.value)); + continue; + } + + if (child.nodes) { + walk(child, node); + } + } + + return queue; + }; + + return utils.flatten(walk(ast)); +}; + +module.exports = expand; + + +/***/ }), +/* 792 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const stringify = __webpack_require__(785); + +/** + * Constants + */ + +const { + MAX_LENGTH, + CHAR_BACKSLASH, /* \ */ + CHAR_BACKTICK, /* ` */ + CHAR_COMMA, /* , */ + CHAR_DOT, /* . */ + CHAR_LEFT_PARENTHESES, /* ( */ + CHAR_RIGHT_PARENTHESES, /* ) */ + CHAR_LEFT_CURLY_BRACE, /* { */ + CHAR_RIGHT_CURLY_BRACE, /* } */ + CHAR_LEFT_SQUARE_BRACKET, /* [ */ + CHAR_RIGHT_SQUARE_BRACKET, /* ] */ + CHAR_DOUBLE_QUOTE, /* " */ + CHAR_SINGLE_QUOTE, /* ' */ + CHAR_NO_BREAK_SPACE, + CHAR_ZERO_WIDTH_NOBREAK_SPACE +} = __webpack_require__(793); + +/** + * parse + */ + +const parse = (input, options = {}) => { + if (typeof input !== 'string') { + throw new TypeError('Expected a string'); + } + + let opts = options || {}; + let max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; + if (input.length > max) { + throw new SyntaxError(`Input length (${input.length}), exceeds max characters (${max})`); + } + + let ast = { type: 'root', input, nodes: [] }; + let stack = [ast]; + let block = ast; + let prev = ast; + let brackets = 0; + let length = input.length; + let index = 0; + let depth = 0; + let value; + let memo = {}; + + /** + * Helpers + */ + + const advance = () => input[index++]; + const push = node => { + if (node.type === 'text' && prev.type === 'dot') { + prev.type = 'text'; + } + + if (prev && prev.type === 'text' && node.type === 'text') { + prev.value += node.value; + return; + } + + block.nodes.push(node); + node.parent = block; + node.prev = prev; + prev = node; + return node; + }; + + push({ type: 'bos' }); + + while (index < length) { + block = stack[stack.length - 1]; + value = advance(); + + /** + * Invalid chars + */ + + if (value === CHAR_ZERO_WIDTH_NOBREAK_SPACE || value === CHAR_NO_BREAK_SPACE) { + continue; + } + + /** + * Escaped chars + */ + + if (value === CHAR_BACKSLASH) { + push({ type: 'text', value: (options.keepEscaping ? value : '') + advance() }); + continue; + } + + /** + * Right square bracket (literal): ']' + */ + + if (value === CHAR_RIGHT_SQUARE_BRACKET) { + push({ type: 'text', value: '\\' + value }); + continue; + } + + /** + * Left square bracket: '[' + */ + + if (value === CHAR_LEFT_SQUARE_BRACKET) { + brackets++; + + let closed = true; + let next; + + while (index < length && (next = advance())) { + value += next; + + if (next === CHAR_LEFT_SQUARE_BRACKET) { + brackets++; + continue; + } + + if (next === CHAR_BACKSLASH) { + value += advance(); + continue; + } + + if (next === CHAR_RIGHT_SQUARE_BRACKET) { + brackets--; + + if (brackets === 0) { + break; + } + } + } + + push({ type: 'text', value }); + continue; + } + + /** + * Parentheses + */ + + if (value === CHAR_LEFT_PARENTHESES) { + block = push({ type: 'paren', nodes: [] }); + stack.push(block); + push({ type: 'text', value }); + continue; + } + + if (value === CHAR_RIGHT_PARENTHESES) { + if (block.type !== 'paren') { + push({ type: 'text', value }); + continue; + } + block = stack.pop(); + push({ type: 'text', value }); + block = stack[stack.length - 1]; + continue; + } + + /** + * Quotes: '|"|` + */ + + if (value === CHAR_DOUBLE_QUOTE || value === CHAR_SINGLE_QUOTE || value === CHAR_BACKTICK) { + let open = value; + let next; + + if (options.keepQuotes !== true) { + value = ''; + } + + while (index < length && (next = advance())) { + if (next === CHAR_BACKSLASH) { + value += next + advance(); + continue; + } + + if (next === open) { + if (options.keepQuotes === true) value += next; + break; + } + + value += next; + } + + push({ type: 'text', value }); + continue; + } + + /** + * Left curly brace: '{' + */ + + if (value === CHAR_LEFT_CURLY_BRACE) { + depth++; + + let dollar = prev.value && prev.value.slice(-1) === '$' || block.dollar === true; + let brace = { + type: 'brace', + open: true, + close: false, + dollar, + depth, + commas: 0, + ranges: 0, + nodes: [] + }; + + block = push(brace); + stack.push(block); + push({ type: 'open', value }); + continue; + } + + /** + * Right curly brace: '}' + */ + + if (value === CHAR_RIGHT_CURLY_BRACE) { + if (block.type !== 'brace') { + push({ type: 'text', value }); + continue; + } + + let type = 'close'; + block = stack.pop(); + block.close = true; + + push({ type, value }); + depth--; + + block = stack[stack.length - 1]; + continue; + } + + /** + * Comma: ',' + */ + + if (value === CHAR_COMMA && depth > 0) { + if (block.ranges > 0) { + block.ranges = 0; + let open = block.nodes.shift(); + block.nodes = [open, { type: 'text', value: stringify(block) }]; + } + + push({ type: 'comma', value }); + block.commas++; + continue; + } + + /** + * Dot: '.' + */ + + if (value === CHAR_DOT && depth > 0 && block.commas === 0) { + let siblings = block.nodes; + + if (depth === 0 || siblings.length === 0) { + push({ type: 'text', value }); + continue; + } + + if (prev.type === 'dot') { + block.range = []; + prev.value += value; + prev.type = 'range'; + + if (block.nodes.length !== 3 && block.nodes.length !== 5) { + block.invalid = true; + block.ranges = 0; + prev.type = 'text'; + continue; + } + + block.ranges++; + block.args = []; + continue; + } + + if (prev.type === 'range') { + siblings.pop(); + + let before = siblings[siblings.length - 1]; + before.value += prev.value + value; + prev = before; + block.ranges--; + continue; + } + + push({ type: 'dot', value }); + continue; + } + + /** + * Text + */ + + push({ type: 'text', value }); + } + + // Mark imbalanced braces and brackets as invalid + do { + block = stack.pop(); + + if (block.type !== 'root') { + block.nodes.forEach(node => { + if (!node.nodes) { + if (node.type === 'open') node.isOpen = true; + if (node.type === 'close') node.isClose = true; + if (!node.nodes) node.type = 'text'; + node.invalid = true; + } + }); + + // get the location of the block on parent.nodes (block's siblings) + let parent = stack[stack.length - 1]; + let index = parent.nodes.indexOf(block); + // replace the (invalid) block with it's nodes + parent.nodes.splice(index, 1, ...block.nodes); + } + } while (stack.length > 0); + + push({ type: 'eos' }); + return ast; +}; + +module.exports = parse; + + +/***/ }), +/* 793 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = { + MAX_LENGTH: 1024 * 64, + + // Digits + CHAR_0: '0', /* 0 */ + CHAR_9: '9', /* 9 */ + + // Alphabet chars. + CHAR_UPPERCASE_A: 'A', /* A */ + CHAR_LOWERCASE_A: 'a', /* a */ + CHAR_UPPERCASE_Z: 'Z', /* Z */ + CHAR_LOWERCASE_Z: 'z', /* z */ + + CHAR_LEFT_PARENTHESES: '(', /* ( */ + CHAR_RIGHT_PARENTHESES: ')', /* ) */ + + CHAR_ASTERISK: '*', /* * */ + + // Non-alphabetic chars. + CHAR_AMPERSAND: '&', /* & */ + CHAR_AT: '@', /* @ */ + CHAR_BACKSLASH: '\\', /* \ */ + CHAR_BACKTICK: '`', /* ` */ + CHAR_CARRIAGE_RETURN: '\r', /* \r */ + CHAR_CIRCUMFLEX_ACCENT: '^', /* ^ */ + CHAR_COLON: ':', /* : */ + CHAR_COMMA: ',', /* , */ + CHAR_DOLLAR: '$', /* . */ + CHAR_DOT: '.', /* . */ + CHAR_DOUBLE_QUOTE: '"', /* " */ + CHAR_EQUAL: '=', /* = */ + CHAR_EXCLAMATION_MARK: '!', /* ! */ + CHAR_FORM_FEED: '\f', /* \f */ + CHAR_FORWARD_SLASH: '/', /* / */ + CHAR_HASH: '#', /* # */ + CHAR_HYPHEN_MINUS: '-', /* - */ + CHAR_LEFT_ANGLE_BRACKET: '<', /* < */ + CHAR_LEFT_CURLY_BRACE: '{', /* { */ + CHAR_LEFT_SQUARE_BRACKET: '[', /* [ */ + CHAR_LINE_FEED: '\n', /* \n */ + CHAR_NO_BREAK_SPACE: '\u00A0', /* \u00A0 */ + CHAR_PERCENT: '%', /* % */ + CHAR_PLUS: '+', /* + */ + CHAR_QUESTION_MARK: '?', /* ? */ + CHAR_RIGHT_ANGLE_BRACKET: '>', /* > */ + CHAR_RIGHT_CURLY_BRACE: '}', /* } */ + CHAR_RIGHT_SQUARE_BRACKET: ']', /* ] */ + CHAR_SEMICOLON: ';', /* ; */ + CHAR_SINGLE_QUOTE: '\'', /* ' */ + CHAR_SPACE: ' ', /* */ + CHAR_TAB: '\t', /* \t */ + CHAR_UNDERSCORE: '_', /* _ */ + CHAR_VERTICAL_LINE: '|', /* | */ + CHAR_ZERO_WIDTH_NOBREAK_SPACE: '\uFEFF' /* \uFEFF */ +}; + + +/***/ }), +/* 794 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.merge = void 0; +const merge2 = __webpack_require__(146); +function merge(streams) { + const mergedStream = merge2(streams); + streams.forEach((stream) => { + stream.once('error', (error) => mergedStream.emit('error', error)); + }); + mergedStream.once('close', () => propagateCloseEventToSources(streams)); + mergedStream.once('end', () => propagateCloseEventToSources(streams)); + return mergedStream; +} +exports.merge = merge; +function propagateCloseEventToSources(streams) { + streams.forEach((stream) => stream.emit('close')); +} + + +/***/ }), +/* 795 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEmpty = exports.isString = void 0; +function isString(input) { + return typeof input === 'string'; +} +exports.isString = isString; +function isEmpty(input) { + return input === ''; +} +exports.isEmpty = isEmpty; + + +/***/ }), +/* 796 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const stream_1 = __webpack_require__(797); +const provider_1 = __webpack_require__(799); +class ProviderAsync extends provider_1.default { + constructor() { + super(...arguments); + this._reader = new stream_1.default(this._settings); + } + read(task) { + const root = this._getRootDirectory(task); + const options = this._getReaderOptions(task); + const entries = []; + return new Promise((resolve, reject) => { + const stream = this.api(root, task, options); + stream.once('error', reject); + stream.on('data', (entry) => entries.push(options.transform(entry))); + stream.once('end', () => resolve(entries)); + }); + } + api(root, task, options) { + if (task.dynamic) { + return this._reader.dynamic(root, options); + } + return this._reader.static(task.patterns, options); + } +} +exports.default = ProviderAsync; + + +/***/ }), +/* 797 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const stream_1 = __webpack_require__(138); +const fsStat = __webpack_require__(195); +const fsWalk = __webpack_require__(200); +const reader_1 = __webpack_require__(798); +class ReaderStream extends reader_1.default { + constructor() { + super(...arguments); + this._walkStream = fsWalk.walkStream; + this._stat = fsStat.stat; + } + dynamic(root, options) { + return this._walkStream(root, options); + } + static(patterns, options) { + const filepaths = patterns.map(this._getFullEntryPath, this); + const stream = new stream_1.PassThrough({ objectMode: true }); + stream._write = (index, _enc, done) => { + return this._getEntry(filepaths[index], patterns[index], options) + .then((entry) => { + if (entry !== null && options.entryFilter(entry)) { + stream.push(entry); + } + if (index === filepaths.length - 1) { + stream.end(); + } + done(); + }) + .catch(done); + }; + for (let i = 0; i < filepaths.length; i++) { + stream.write(i); + } + return stream; + } + _getEntry(filepath, pattern, options) { + return this._getStat(filepath) + .then((stats) => this._makeEntry(stats, pattern)) + .catch((error) => { + if (options.errorFilter(error)) { + return null; + } + throw error; + }); + } + _getStat(filepath) { + return new Promise((resolve, reject) => { + this._stat(filepath, this._fsStatSettings, (error, stats) => { + return error === null ? resolve(stats) : reject(error); + }); + }); + } +} +exports.default = ReaderStream; + + +/***/ }), +/* 798 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const path = __webpack_require__(4); +const fsStat = __webpack_require__(195); +const utils = __webpack_require__(777); +class Reader { + constructor(_settings) { + this._settings = _settings; + this._fsStatSettings = new fsStat.Settings({ + followSymbolicLink: this._settings.followSymbolicLinks, + fs: this._settings.fs, + throwErrorOnBrokenSymbolicLink: this._settings.followSymbolicLinks + }); + } + _getFullEntryPath(filepath) { + return path.resolve(this._settings.cwd, filepath); + } + _makeEntry(stats, pattern) { + const entry = { + name: pattern, + path: pattern, + dirent: utils.fs.createDirentFromStats(pattern, stats) + }; + if (this._settings.stats) { + entry.stats = stats; + } + return entry; + } + _isFatalError(error) { + return !utils.errno.isEnoentCodeError(error) && !this._settings.suppressErrors; + } +} +exports.default = Reader; + + +/***/ }), +/* 799 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const path = __webpack_require__(4); +const deep_1 = __webpack_require__(800); +const entry_1 = __webpack_require__(803); +const error_1 = __webpack_require__(804); +const entry_2 = __webpack_require__(805); +class Provider { + constructor(_settings) { + this._settings = _settings; + this.errorFilter = new error_1.default(this._settings); + this.entryFilter = new entry_1.default(this._settings, this._getMicromatchOptions()); + this.deepFilter = new deep_1.default(this._settings, this._getMicromatchOptions()); + this.entryTransformer = new entry_2.default(this._settings); + } + _getRootDirectory(task) { + return path.resolve(this._settings.cwd, task.base); + } + _getReaderOptions(task) { + const basePath = task.base === '.' ? '' : task.base; + return { + basePath, + pathSegmentSeparator: '/', + concurrency: this._settings.concurrency, + deepFilter: this.deepFilter.getFilter(basePath, task.positive, task.negative), + entryFilter: this.entryFilter.getFilter(task.positive, task.negative), + errorFilter: this.errorFilter.getFilter(), + followSymbolicLinks: this._settings.followSymbolicLinks, + fs: this._settings.fs, + stats: this._settings.stats, + throwErrorOnBrokenSymbolicLink: this._settings.throwErrorOnBrokenSymbolicLink, + transform: this.entryTransformer.getTransformer() + }; + } + _getMicromatchOptions() { + return { + dot: this._settings.dot, + matchBase: this._settings.baseNameMatch, + nobrace: !this._settings.braceExpansion, + nocase: !this._settings.caseSensitiveMatch, + noext: !this._settings.extglob, + noglobstar: !this._settings.globstar, + posix: true, + strictSlashes: false + }; + } +} +exports.default = Provider; + + +/***/ }), +/* 800 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +const partial_1 = __webpack_require__(801); +class DeepFilter { + constructor(_settings, _micromatchOptions) { + this._settings = _settings; + this._micromatchOptions = _micromatchOptions; + } + getFilter(basePath, positive, negative) { + const matcher = this._getMatcher(positive); + const negativeRe = this._getNegativePatternsRe(negative); + return (entry) => this._filter(basePath, entry, matcher, negativeRe); + } + _getMatcher(patterns) { + return new partial_1.default(patterns, this._settings, this._micromatchOptions); + } + _getNegativePatternsRe(patterns) { + const affectDepthOfReadingPatterns = patterns.filter(utils.pattern.isAffectDepthOfReadingPattern); + return utils.pattern.convertPatternsToRe(affectDepthOfReadingPatterns, this._micromatchOptions); + } + _filter(basePath, entry, matcher, negativeRe) { + if (this._isSkippedByDeep(basePath, entry.path)) { + return false; + } + if (this._isSkippedSymbolicLink(entry)) { + return false; + } + const filepath = utils.path.removeLeadingDotSegment(entry.path); + if (this._isSkippedByPositivePatterns(filepath, matcher)) { + return false; + } + return this._isSkippedByNegativePatterns(filepath, negativeRe); + } + _isSkippedByDeep(basePath, entryPath) { + /** + * Avoid unnecessary depth calculations when it doesn't matter. + */ + if (this._settings.deep === Infinity) { + return false; + } + return this._getEntryLevel(basePath, entryPath) >= this._settings.deep; + } + _getEntryLevel(basePath, entryPath) { + const entryPathDepth = entryPath.split('/').length; + if (basePath === '') { + return entryPathDepth; + } + const basePathDepth = basePath.split('/').length; + return entryPathDepth - basePathDepth; + } + _isSkippedSymbolicLink(entry) { + return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink(); + } + _isSkippedByPositivePatterns(entryPath, matcher) { + return !this._settings.baseNameMatch && !matcher.match(entryPath); + } + _isSkippedByNegativePatterns(entryPath, patternsRe) { + return !utils.pattern.matchAny(entryPath, patternsRe); + } +} +exports.default = DeepFilter; + + +/***/ }), +/* 801 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const matcher_1 = __webpack_require__(802); +class PartialMatcher extends matcher_1.default { + match(filepath) { + const parts = filepath.split('/'); + const levels = parts.length; + const patterns = this._storage.filter((info) => !info.complete || info.segments.length > levels); + for (const pattern of patterns) { + const section = pattern.sections[0]; + /** + * In this case, the pattern has a globstar and we must read all directories unconditionally, + * but only if the level has reached the end of the first group. + * + * fixtures/{a,b}/** + * ^ true/false ^ always true + */ + if (!pattern.complete && levels > section.length) { + return true; + } + const match = parts.every((part, index) => { + const segment = pattern.segments[index]; + if (segment.dynamic && segment.patternRe.test(part)) { + return true; + } + if (!segment.dynamic && segment.pattern === part) { + return true; + } + return false; + }); + if (match) { + return true; + } + } + return false; + } +} +exports.default = PartialMatcher; + + +/***/ }), +/* 802 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class Matcher { + constructor(_patterns, _settings, _micromatchOptions) { + this._patterns = _patterns; + this._settings = _settings; + this._micromatchOptions = _micromatchOptions; + this._storage = []; + this._fillStorage(); + } + _fillStorage() { + /** + * The original pattern may include `{,*,**,a/*}`, which will lead to problems with matching (unresolved level). + * So, before expand patterns with brace expansion into separated patterns. + */ + const patterns = utils.pattern.expandPatternsWithBraceExpansion(this._patterns); + for (const pattern of patterns) { + const segments = this._getPatternSegments(pattern); + const sections = this._splitSegmentsIntoSections(segments); + this._storage.push({ + complete: sections.length <= 1, + pattern, + segments, + sections + }); + } + } + _getPatternSegments(pattern) { + const parts = utils.pattern.getPatternParts(pattern, this._micromatchOptions); + return parts.map((part) => { + const dynamic = utils.pattern.isDynamicPattern(part, this._settings); + if (!dynamic) { + return { + dynamic: false, + pattern: part + }; + } + return { + dynamic: true, + pattern: part, + patternRe: utils.pattern.makeRe(part, this._micromatchOptions) + }; + }); + } + _splitSegmentsIntoSections(segments) { + return utils.array.splitWhen(segments, (segment) => segment.dynamic && utils.pattern.hasGlobStar(segment.pattern)); + } +} +exports.default = Matcher; + + +/***/ }), +/* 803 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class EntryFilter { + constructor(_settings, _micromatchOptions) { + this._settings = _settings; + this._micromatchOptions = _micromatchOptions; + this.index = new Map(); + } + getFilter(positive, negative) { + const positiveRe = utils.pattern.convertPatternsToRe(positive, this._micromatchOptions); + const negativeRe = utils.pattern.convertPatternsToRe(negative, this._micromatchOptions); + return (entry) => this._filter(entry, positiveRe, negativeRe); + } + _filter(entry, positiveRe, negativeRe) { + if (this._settings.unique && this._isDuplicateEntry(entry)) { + return false; + } + if (this._onlyFileFilter(entry) || this._onlyDirectoryFilter(entry)) { + return false; + } + if (this._isSkippedByAbsoluteNegativePatterns(entry.path, negativeRe)) { + return false; + } + const filepath = this._settings.baseNameMatch ? entry.name : entry.path; + const isMatched = this._isMatchToPatterns(filepath, positiveRe) && !this._isMatchToPatterns(entry.path, negativeRe); + if (this._settings.unique && isMatched) { + this._createIndexRecord(entry); + } + return isMatched; + } + _isDuplicateEntry(entry) { + return this.index.has(entry.path); + } + _createIndexRecord(entry) { + this.index.set(entry.path, undefined); + } + _onlyFileFilter(entry) { + return this._settings.onlyFiles && !entry.dirent.isFile(); + } + _onlyDirectoryFilter(entry) { + return this._settings.onlyDirectories && !entry.dirent.isDirectory(); + } + _isSkippedByAbsoluteNegativePatterns(entryPath, patternsRe) { + if (!this._settings.absolute) { + return false; + } + const fullpath = utils.path.makeAbsolute(this._settings.cwd, entryPath); + return utils.pattern.matchAny(fullpath, patternsRe); + } + _isMatchToPatterns(entryPath, patternsRe) { + const filepath = utils.path.removeLeadingDotSegment(entryPath); + return utils.pattern.matchAny(filepath, patternsRe); + } +} +exports.default = EntryFilter; - for (let i = 0; i < arguments.length; i++) { - results[i] = arguments[i]; - } - resolve(results); - } else { - resolve(result); - } - }); - } +/***/ }), +/* 804 */ +/***/ (function(module, exports, __webpack_require__) { - fn.apply(this, args); - }); -}; +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class ErrorFilter { + constructor(_settings) { + this._settings = _settings; + } + getFilter() { + return (error) => this._isNonFatalError(error); + } + _isNonFatalError(error) { + return utils.errno.isEnoentCodeError(error) || this._settings.suppressErrors; + } +} +exports.default = ErrorFilter; -module.exports = (obj, opts) => { - opts = Object.assign({ - exclude: [/.+(Sync|Stream)$/], - errorFirst: true, - promiseModule: Promise - }, opts); - const filter = key => { - const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key); - return opts.include ? opts.include.some(match) : !opts.exclude.some(match); - }; +/***/ }), +/* 805 */ +/***/ (function(module, exports, __webpack_require__) { - let ret; - if (typeof obj === 'function') { - ret = function () { - if (opts.excludeMain) { - return obj.apply(this, arguments); - } +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class EntryTransformer { + constructor(_settings) { + this._settings = _settings; + } + getTransformer() { + return (entry) => this._transform(entry); + } + _transform(entry) { + let filepath = entry.path; + if (this._settings.absolute) { + filepath = utils.path.makeAbsolute(this._settings.cwd, filepath); + filepath = utils.path.unixify(filepath); + } + if (this._settings.markDirectories && entry.dirent.isDirectory()) { + filepath += '/'; + } + if (!this._settings.objectMode) { + return filepath; + } + return Object.assign(Object.assign({}, entry), { path: filepath }); + } +} +exports.default = EntryTransformer; - return processFn(obj, opts).apply(this, arguments); - }; - } else { - ret = Object.create(Object.getPrototypeOf(obj)); - } - for (const key in obj) { // eslint-disable-line guard-for-in - const x = obj[key]; - ret[key] = typeof x === 'function' && filter(key) ? processFn(x, opts) : x; - } +/***/ }), +/* 806 */ +/***/ (function(module, exports, __webpack_require__) { - return ret; -}; +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const stream_1 = __webpack_require__(138); +const stream_2 = __webpack_require__(797); +const provider_1 = __webpack_require__(799); +class ProviderStream extends provider_1.default { + constructor() { + super(...arguments); + this._reader = new stream_2.default(this._settings); + } + read(task) { + const root = this._getRootDirectory(task); + const options = this._getReaderOptions(task); + const source = this.api(root, task, options); + const destination = new stream_1.Readable({ objectMode: true, read: () => { } }); + source + .once('error', (error) => destination.emit('error', error)) + .on('data', (entry) => destination.emit('data', options.transform(entry))) + .once('end', () => destination.emit('end')); + destination + .once('close', () => source.destroy()); + return destination; + } + api(root, task, options) { + if (task.dynamic) { + return this._reader.dynamic(root, options); + } + return this._reader.static(task.patterns, options); + } +} +exports.default = ProviderStream; /***/ }), -/* 780 */ +/* 807 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const sync_1 = __webpack_require__(808); +const provider_1 = __webpack_require__(799); +class ProviderSync extends provider_1.default { + constructor() { + super(...arguments); + this._reader = new sync_1.default(this._settings); + } + read(task) { + const root = this._getRootDirectory(task); + const options = this._getReaderOptions(task); + const entries = this.api(root, task, options); + return entries.map(options.transform); + } + api(root, task, options) { + if (task.dynamic) { + return this._reader.dynamic(root, options); + } + return this._reader.static(task.patterns, options); + } +} +exports.default = ProviderSync; + + +/***/ }), +/* 808 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const fsStat = __webpack_require__(195); +const fsWalk = __webpack_require__(200); +const reader_1 = __webpack_require__(798); +class ReaderSync extends reader_1.default { + constructor() { + super(...arguments); + this._walkSync = fsWalk.walkSync; + this._statSync = fsStat.statSync; + } + dynamic(root, options) { + return this._walkSync(root, options); + } + static(patterns, options) { + const entries = []; + for (const pattern of patterns) { + const filepath = this._getFullEntryPath(pattern); + const entry = this._getEntry(filepath, pattern, options); + if (entry === null || !options.entryFilter(entry)) { + continue; + } + entries.push(entry); + } + return entries; + } + _getEntry(filepath, pattern, options) { + try { + const stats = this._getStat(filepath); + return this._makeEntry(stats, pattern); + } + catch (error) { + if (options.errorFilter(error)) { + return null; + } + throw error; + } + } + _getStat(filepath) { + return this._statSync(filepath, this._fsStatSettings); + } +} +exports.default = ReaderSync; + + +/***/ }), +/* 809 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DEFAULT_FILE_SYSTEM_ADAPTER = void 0; +const fs = __webpack_require__(134); +const os = __webpack_require__(121); +/** + * The `os.cpus` method can return zero. We expect the number of cores to be greater than zero. + * https://github.com/nodejs/node/blob/7faeddf23a98c53896f8b574a6e66589e8fb1eb8/lib/os.js#L106-L107 + */ +const CPU_COUNT = Math.max(os.cpus().length, 1); +exports.DEFAULT_FILE_SYSTEM_ADAPTER = { + lstat: fs.lstat, + lstatSync: fs.lstatSync, + stat: fs.stat, + statSync: fs.statSync, + readdir: fs.readdir, + readdirSync: fs.readdirSync +}; +class Settings { + constructor(_options = {}) { + this._options = _options; + this.absolute = this._getValue(this._options.absolute, false); + this.baseNameMatch = this._getValue(this._options.baseNameMatch, false); + this.braceExpansion = this._getValue(this._options.braceExpansion, true); + this.caseSensitiveMatch = this._getValue(this._options.caseSensitiveMatch, true); + this.concurrency = this._getValue(this._options.concurrency, CPU_COUNT); + this.cwd = this._getValue(this._options.cwd, process.cwd()); + this.deep = this._getValue(this._options.deep, Infinity); + this.dot = this._getValue(this._options.dot, false); + this.extglob = this._getValue(this._options.extglob, true); + this.followSymbolicLinks = this._getValue(this._options.followSymbolicLinks, true); + this.fs = this._getFileSystemMethods(this._options.fs); + this.globstar = this._getValue(this._options.globstar, true); + this.ignore = this._getValue(this._options.ignore, []); + this.markDirectories = this._getValue(this._options.markDirectories, false); + this.objectMode = this._getValue(this._options.objectMode, false); + this.onlyDirectories = this._getValue(this._options.onlyDirectories, false); + this.onlyFiles = this._getValue(this._options.onlyFiles, true); + this.stats = this._getValue(this._options.stats, false); + this.suppressErrors = this._getValue(this._options.suppressErrors, false); + this.throwErrorOnBrokenSymbolicLink = this._getValue(this._options.throwErrorOnBrokenSymbolicLink, false); + this.unique = this._getValue(this._options.unique, true); + if (this.onlyDirectories) { + this.onlyFiles = false; + } + if (this.stats) { + this.objectMode = true; + } + } + _getValue(option, value) { + return option === undefined ? value : option; + } + _getFileSystemMethods(methods = {}) { + return Object.assign(Object.assign({}, exports.DEFAULT_FILE_SYSTEM_ADAPTER), methods); + } +} +exports.default = Settings; + + +/***/ }), +/* 810 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +const {promisify} = __webpack_require__(112); const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(572); -const gitIgnore = __webpack_require__(781); -const pify = __webpack_require__(779); -const slash = __webpack_require__(782); +const fastGlob = __webpack_require__(775); +const gitIgnore = __webpack_require__(235); +const slash = __webpack_require__(236); const DEFAULT_IGNORE = [ '**/node_modules/**', - '**/bower_components/**', '**/flow-typed/**', '**/coverage/**', '**/.git' ]; -const readFileP = pify(fs.readFile); +const readFileP = promisify(fs.readFile); const mapGitIgnorePatternTo = base => ignore => { if (ignore.startsWith('!')) { - return '!' + path.posix.join(base, ignore.substr(1)); + return '!' + path.posix.join(base, ignore.slice(1)); } return path.posix.join(base, ignore); }; -const parseGitIgnore = (content, opts) => { - const base = slash(path.relative(opts.cwd, path.dirname(opts.fileName))); +const parseGitIgnore = (content, options) => { + const base = slash(path.relative(options.cwd, path.dirname(options.fileName))); return content .split(/\r?\n/) .filter(Boolean) - .filter(l => l.charAt(0) !== '#') + .filter(line => !line.startsWith('#')) .map(mapGitIgnorePatternTo(base)); }; const reduceIgnore = files => { - return files.reduce((ignores, file) => { + const ignores = gitIgnore(); + for (const file of files) { ignores.add(parseGitIgnore(file.content, { cwd: file.cwd, fileName: file.filePath })); - return ignores; - }, gitIgnore()); + } + + return ignores; +}; + +const ensureAbsolutePathForCwd = (cwd, p) => { + cwd = slash(cwd); + if (path.isAbsolute(p)) { + if (slash(p).startsWith(cwd)) { + return p; + } + + throw new Error(`Path ${p} is not in cwd ${cwd}`); + } + + return path.join(cwd, p); }; const getIsIgnoredPredecate = (ignores, cwd) => { - return p => ignores.ignores(slash(path.relative(cwd, p))); + return p => ignores.ignores(slash(path.relative(cwd, ensureAbsolutePathForCwd(cwd, p.path || p)))); }; -const getFile = (file, cwd) => { +const getFile = async (file, cwd) => { const filePath = path.join(cwd, file); - return readFileP(filePath, 'utf8') - .then(content => ({ - content, - cwd, - filePath - })); + const content = await readFileP(filePath, 'utf8'); + + return { + cwd, + filePath, + content + }; }; const getFileSync = (file, cwd) => { @@ -90686,490 +93669,103 @@ const getFileSync = (file, cwd) => { const content = fs.readFileSync(filePath, 'utf8'); return { - content, cwd, - filePath + filePath, + content }; }; -const normalizeOpts = opts => { - opts = opts || {}; - const ignore = opts.ignore || []; - const cwd = opts.cwd || process.cwd(); +const normalizeOptions = ({ + ignore = [], + cwd = slash(process.cwd()) +} = {}) => { return {ignore, cwd}; }; -module.exports = o => { - const opts = normalizeOpts(o); - - return fastGlob('**/.gitignore', {ignore: DEFAULT_IGNORE.concat(opts.ignore), cwd: opts.cwd}) - .then(paths => Promise.all(paths.map(file => getFile(file, opts.cwd)))) - .then(files => reduceIgnore(files)) - .then(ignores => getIsIgnoredPredecate(ignores, opts.cwd)); -}; +module.exports = async options => { + options = normalizeOptions(options); -module.exports.sync = o => { - const opts = normalizeOpts(o); + const paths = await fastGlob('**/.gitignore', { + ignore: DEFAULT_IGNORE.concat(options.ignore), + cwd: options.cwd + }); - const paths = fastGlob.sync('**/.gitignore', {ignore: DEFAULT_IGNORE.concat(opts.ignore), cwd: opts.cwd}); - const files = paths.map(file => getFileSync(file, opts.cwd)); + const files = await Promise.all(paths.map(file => getFile(file, options.cwd))); const ignores = reduceIgnore(files); - return getIsIgnoredPredecate(ignores, opts.cwd); -}; - - -/***/ }), -/* 781 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -module.exports = function () { - return new IgnoreBase(); + return getIsIgnoredPredecate(ignores, options.cwd); }; -// A simple implementation of make-array -function make_array(subject) { - return Array.isArray(subject) ? subject : [subject]; -} - -var REGEX_BLANK_LINE = /^\s+$/; -var REGEX_LEADING_EXCAPED_EXCLAMATION = /^\\\!/; -var REGEX_LEADING_EXCAPED_HASH = /^\\#/; -var SLASH = '/'; -var KEY_IGNORE = typeof Symbol !== 'undefined' ? Symbol.for('node-ignore') -/* istanbul ignore next */ -: 'node-ignore'; - -var IgnoreBase = function () { - function IgnoreBase() { - _classCallCheck(this, IgnoreBase); - - this._rules = []; - this[KEY_IGNORE] = true; - this._initCache(); - } - - _createClass(IgnoreBase, [{ - key: '_initCache', - value: function _initCache() { - this._cache = {}; - } - - // @param {Array.|string|Ignore} pattern - - }, { - key: 'add', - value: function add(pattern) { - this._added = false; - - if (typeof pattern === 'string') { - pattern = pattern.split(/\r?\n/g); - } - - make_array(pattern).forEach(this._addPattern, this); - - // Some rules have just added to the ignore, - // making the behavior changed. - if (this._added) { - this._initCache(); - } - - return this; - } - - // legacy - - }, { - key: 'addPattern', - value: function addPattern(pattern) { - return this.add(pattern); - } - }, { - key: '_addPattern', - value: function _addPattern(pattern) { - // #32 - if (pattern && pattern[KEY_IGNORE]) { - this._rules = this._rules.concat(pattern._rules); - this._added = true; - return; - } - - if (this._checkPattern(pattern)) { - var rule = this._createRule(pattern); - this._added = true; - this._rules.push(rule); - } - } - }, { - key: '_checkPattern', - value: function _checkPattern(pattern) { - // > A blank line matches no files, so it can serve as a separator for readability. - return pattern && typeof pattern === 'string' && !REGEX_BLANK_LINE.test(pattern) - - // > A line starting with # serves as a comment. - && pattern.indexOf('#') !== 0; - } - }, { - key: 'filter', - value: function filter(paths) { - var _this = this; - - return make_array(paths).filter(function (path) { - return _this._filter(path); - }); - } - }, { - key: 'createFilter', - value: function createFilter() { - var _this2 = this; - - return function (path) { - return _this2._filter(path); - }; - } - }, { - key: 'ignores', - value: function ignores(path) { - return !this._filter(path); - } - }, { - key: '_createRule', - value: function _createRule(pattern) { - var origin = pattern; - var negative = false; - - // > An optional prefix "!" which negates the pattern; - if (pattern.indexOf('!') === 0) { - negative = true; - pattern = pattern.substr(1); - } - - pattern = pattern - // > Put a backslash ("\") in front of the first "!" for patterns that begin with a literal "!", for example, `"\!important!.txt"`. - .replace(REGEX_LEADING_EXCAPED_EXCLAMATION, '!') - // > Put a backslash ("\") in front of the first hash for patterns that begin with a hash. - .replace(REGEX_LEADING_EXCAPED_HASH, '#'); - - var regex = make_regex(pattern, negative); - - return { - origin: origin, - pattern: pattern, - negative: negative, - regex: regex - }; - } - - // @returns `Boolean` true if the `path` is NOT ignored - - }, { - key: '_filter', - value: function _filter(path, slices) { - if (!path) { - return false; - } - - if (path in this._cache) { - return this._cache[path]; - } - - if (!slices) { - // path/to/a.js - // ['path', 'to', 'a.js'] - slices = path.split(SLASH); - } - - slices.pop(); - - return this._cache[path] = slices.length - // > It is not possible to re-include a file if a parent directory of that file is excluded. - // If the path contains a parent directory, check the parent first - ? this._filter(slices.join(SLASH) + SLASH, slices) && this._test(path) - - // Or only test the path - : this._test(path); - } - - // @returns {Boolean} true if a file is NOT ignored - - }, { - key: '_test', - value: function _test(path) { - // Explicitly define variable type by setting matched to `0` - var matched = 0; - - this._rules.forEach(function (rule) { - // if matched = true, then we only test negative rules - // if matched = false, then we test non-negative rules - if (!(matched ^ rule.negative)) { - matched = rule.negative ^ rule.regex.test(path); - } - }); - - return !matched; - } - }]); - - return IgnoreBase; -}(); - -// > If the pattern ends with a slash, -// > it is removed for the purpose of the following description, -// > but it would only find a match with a directory. -// > In other words, foo/ will match a directory foo and paths underneath it, -// > but will not match a regular file or a symbolic link foo -// > (this is consistent with the way how pathspec works in general in Git). -// '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`' -// -> ignore-rules will not deal with it, because it costs extra `fs.stat` call -// you could use option `mark: true` with `glob` - -// '`foo/`' should not continue with the '`..`' - - -var DEFAULT_REPLACER_PREFIX = [ - -// > Trailing spaces are ignored unless they are quoted with backslash ("\") -[ -// (a\ ) -> (a ) -// (a ) -> (a) -// (a \ ) -> (a ) -/\\?\s+$/, function (match) { - return match.indexOf('\\') === 0 ? ' ' : ''; -}], - -// replace (\ ) with ' ' -[/\\\s/g, function () { - return ' '; -}], - -// Escape metacharacters -// which is written down by users but means special for regular expressions. - -// > There are 12 characters with special meanings: -// > - the backslash \, -// > - the caret ^, -// > - the dollar sign $, -// > - the period or dot ., -// > - the vertical bar or pipe symbol |, -// > - the question mark ?, -// > - the asterisk or star *, -// > - the plus sign +, -// > - the opening parenthesis (, -// > - the closing parenthesis ), -// > - and the opening square bracket [, -// > - the opening curly brace {, -// > These special characters are often called "metacharacters". -[/[\\\^$.|?*+()\[{]/g, function (match) { - return '\\' + match; -}], - -// leading slash -[ - -// > A leading slash matches the beginning of the pathname. -// > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c". -// A leading slash matches the beginning of the pathname -/^\//, function () { - return '^'; -}], - -// replace special metacharacter slash after the leading slash -[/\//g, function () { - return '\\/'; -}], [ -// > A leading "**" followed by a slash means match in all directories. -// > For example, "**/foo" matches file or directory "foo" anywhere, -// > the same as pattern "foo". -// > "**/foo/bar" matches file or directory "bar" anywhere that is directly under directory "foo". -// Notice that the '*'s have been replaced as '\\*' -/^\^*\\\*\\\*\\\//, - -// '**/foo' <-> 'foo' -function () { - return '^(?:.*\\/)?'; -}]]; - -var DEFAULT_REPLACER_SUFFIX = [ -// starting -[ -// there will be no leading '/' (which has been replaced by section "leading slash") -// If starts with '**', adding a '^' to the regular expression also works -/^(?=[^\^])/, function () { - return !/\/(?!$)/.test(this) - // > If the pattern does not contain a slash /, Git treats it as a shell glob pattern - // Actually, if there is only a trailing slash, git also treats it as a shell glob pattern - ? '(?:^|\\/)' - - // > Otherwise, Git treats the pattern as a shell glob suitable for consumption by fnmatch(3) - : '^'; -}], - -// two globstars -[ -// Use lookahead assertions so that we could match more than one `'/**'` -/\\\/\\\*\\\*(?=\\\/|$)/g, - -// Zero, one or several directories -// should not use '*', or it will be replaced by the next replacer - -// Check if it is not the last `'/**'` -function (match, index, str) { - return index + 6 < str.length - - // case: /**/ - // > A slash followed by two consecutive asterisks then a slash matches zero or more directories. - // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on. - // '/**/' - ? '(?:\\/[^\\/]+)*' +module.exports.sync = options => { + options = normalizeOptions(options); - // case: /** - // > A trailing `"/**"` matches everything inside. + const paths = fastGlob.sync('**/.gitignore', { + ignore: DEFAULT_IGNORE.concat(options.ignore), + cwd: options.cwd + }); - // #21: everything inside but it should not include the current folder - : '\\/.+'; -}], + const files = paths.map(file => getFileSync(file, options.cwd)); + const ignores = reduceIgnore(files); -// intermediate wildcards -[ -// Never replace escaped '*' -// ignore rule '\*' will match the path '*' - -// 'abc.*/' -> go -// 'abc.*' -> skip this rule -/(^|[^\\]+)\\\*(?=.+)/g, - -// '*.js' matches '.js' -// '*.js' doesn't match 'abc' -function (match, p1) { - return p1 + '[^\\/]*'; -}], - -// trailing wildcard -[/(\^|\\\/)?\\\*$/, function (match, p1) { - return (p1 - // '\^': - // '/*' does not match '' - // '/*' does not match everything - - // '\\\/': - // 'abc/*' does not match 'abc/' - ? p1 + '[^/]+' - - // 'a*' matches 'a' - // 'a*' matches 'aa' - : '[^/]*') + '(?=$|\\/$)'; -}], [ -// unescape -/\\\\\\/g, function () { - return '\\'; -}]]; - -var POSITIVE_REPLACERS = [].concat(DEFAULT_REPLACER_PREFIX, [ - -// 'f' -// matches -// - /f(end) -// - /f/ -// - (start)f(end) -// - (start)f/ -// doesn't match -// - oof -// - foo -// pseudo: -// -> (^|/)f(/|$) - -// ending -[ -// 'js' will not match 'js.' -// 'ab' will not match 'abc' -/(?:[^*\/])$/, - -// 'js*' will not match 'a.js' -// 'js/' will not match 'a.js' -// 'js' will match 'a.js' and 'a.js/' -function (match) { - return match + '(?=$|\\/)'; -}]], DEFAULT_REPLACER_SUFFIX); - -var NEGATIVE_REPLACERS = [].concat(DEFAULT_REPLACER_PREFIX, [ - -// #24, #38 -// The MISSING rule of [gitignore docs](https://git-scm.com/docs/gitignore) -// A negative pattern without a trailing wildcard should not -// re-include the things inside that directory. - -// eg: -// ['node_modules/*', '!node_modules'] -// should ignore `node_modules/a.js` -[/(?:[^*])$/, function (match) { - return match + '(?=$|\\/$)'; -}]], DEFAULT_REPLACER_SUFFIX); + return getIsIgnoredPredecate(ignores, options.cwd); +}; -// A simple cache, because an ignore rule only has only one certain meaning -var cache = {}; -// @param {pattern} -function make_regex(pattern, negative) { - var r = cache[pattern]; - if (r) { - return r; - } +/***/ }), +/* 811 */ +/***/ (function(module, exports, __webpack_require__) { - var replacers = negative ? NEGATIVE_REPLACERS : POSITIVE_REPLACERS; +"use strict"; - var source = replacers.reduce(function (prev, current) { - return prev.replace(current[0], current[1].bind(pattern)); - }, pattern); +const {Transform} = __webpack_require__(138); - return cache[pattern] = new RegExp(source, 'i'); +class ObjectTransform extends Transform { + constructor() { + super({ + objectMode: true + }); + } } -// Windows -// -------------------------------------------------------------- -/* istanbul ignore if */ -if ( -// Detect `process` so that it can run in browsers. -typeof process !== 'undefined' && (process.env && process.env.IGNORE_TEST_WIN32 || process.platform === 'win32')) { +class FilterStream extends ObjectTransform { + constructor(filter) { + super(); + this._filter = filter; + } - var filter = IgnoreBase.prototype._filter; - var make_posix = function make_posix(str) { - return (/^\\\\\?\\/.test(str) || /[^\x00-\x80]+/.test(str) ? str : str.replace(/\\/g, '/') - ); - }; + _transform(data, encoding, callback) { + if (this._filter(data)) { + this.push(data); + } - IgnoreBase.prototype._filter = function (path, slices) { - path = make_posix(path); - return filter.call(this, path, slices); - }; + callback(); + } } +class UniqueStream extends ObjectTransform { + constructor() { + super(); + this._pushed = new Set(); + } -/***/ }), -/* 782 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -module.exports = function (str) { - var isExtendedLengthPath = /^\\\\\?\\/.test(str); - var hasNonAscii = /[^\x00-\x80]+/.test(str); + _transform(data, encoding, callback) { + if (!this._pushed.has(data)) { + this.push(data); + this._pushed.add(data); + } - if (isExtendedLengthPath || hasNonAscii) { - return str; + callback(); } +} - return str.replace(/\\/g, '/'); +module.exports = { + FilterStream, + UniqueStream }; /***/ }), -/* 783 */ +/* 812 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 5c81c9100e60..db10d4630ff9 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.json", "compilerOptions": { "incremental": false, "outDir": "target", diff --git a/packages/kbn-utils/src/package_json/index.ts b/packages/kbn-utils/src/package_json/index.ts index 40ce35378074..d9304cee2ca3 100644 --- a/packages/kbn-utils/src/package_json/index.ts +++ b/packages/kbn-utils/src/package_json/index.ts @@ -14,3 +14,7 @@ export const kibanaPackageJson = { __dirname: dirname(resolve(REPO_ROOT, 'package.json')), ...require(resolve(REPO_ROOT, 'package.json')), }; + +export const isKibanaDistributable = () => { + return kibanaPackageJson.build && kibanaPackageJson.build.distributable === true; +}; diff --git a/src/cli/cli.js b/src/cli/cli.js index 4540bf4a3f93..d3bff4f492a8 100644 --- a/src/cli/cli.js +++ b/src/cli/cli.js @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import { pkg } from '../core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import Command from './command'; import serveCommand from './serve/serve'; diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index a494e4538e79..ad83965efde3 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -12,8 +12,7 @@ import { statSync } from 'fs'; import { resolve } from 'path'; import url from 'url'; -import { getConfigPath, fromRoot } from '@kbn/utils'; -import { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; +import { getConfigPath, fromRoot, isKibanaDistributable } from '@kbn/utils'; import { readKeystore } from '../keystore/read_keystore'; function canRequire(path) { @@ -65,9 +64,10 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { delete rawConfig.xpack; } - if (opts.dev) { - set('env', 'development'); + // only used to set cliArgs.envName, we don't want to inject that into the config + delete extraCliOptions.env; + if (opts.dev) { if (!has('elasticsearch.username')) { set('elasticsearch.username', 'kibana_system'); } @@ -184,7 +184,7 @@ export default function (program) { .option('--plugins ', 'an alias for --plugin-dir', pluginDirCollector) .option('--optimize', 'Deprecated, running the optimizer is no longer required'); - if (!IS_KIBANA_DISTRIBUTABLE) { + if (!isKibanaDistributable()) { command .option('--oss', 'Start Kibana without X-Pack') .option( diff --git a/src/cli_encryption_keys/cli_encryption_keys.js b/src/cli_encryption_keys/cli_encryption_keys.js index e922b9354d29..acee81aabb70 100644 --- a/src/cli_encryption_keys/cli_encryption_keys.js +++ b/src/cli_encryption_keys/cli_encryption_keys.js @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { pkg } from '../core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; + import Command from '../cli/command'; import { EncryptionConfig } from './encryption_config'; diff --git a/src/cli_keystore/cli_keystore.js b/src/cli_keystore/cli_keystore.js index b325f685766a..9f44e5d56e9d 100644 --- a/src/cli_keystore/cli_keystore.js +++ b/src/cli_keystore/cli_keystore.js @@ -7,8 +7,8 @@ */ import _ from 'lodash'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; -import { pkg } from '../core/server/utils'; import Command from '../cli/command'; import { Keystore } from '../cli/keystore'; diff --git a/src/cli_plugin/cli.js b/src/cli_plugin/cli.js index 24ccba6a2339..5ef142192c50 100644 --- a/src/cli_plugin/cli.js +++ b/src/cli_plugin/cli.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { pkg } from '../core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import Command from '../cli/command'; import { listCommand } from './list'; import { installCommand } from './install'; diff --git a/src/cli_plugin/install/index.js b/src/cli_plugin/install/index.js index c028facc28e2..2683dd41d2bb 100644 --- a/src/cli_plugin/install/index.js +++ b/src/cli_plugin/install/index.js @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { getConfigPath } from '@kbn/utils'; -import { pkg } from '../../core/server/utils'; +import { getConfigPath, kibanaPackageJson as pkg } from '@kbn/utils'; import { install } from './install'; import { Logger } from '../lib/logger'; import { parse, parseMilliseconds } from './settings'; diff --git a/src/cli_plugin/install/kibana.js b/src/cli_plugin/install/kibana.js index 29cb8df7401b..1de157b951d0 100644 --- a/src/cli_plugin/install/kibana.js +++ b/src/cli_plugin/install/kibana.js @@ -9,7 +9,7 @@ import path from 'path'; import { statSync } from 'fs'; -import { versionSatisfies, cleanVersion } from '../../legacy/utils/version'; +import { versionSatisfies, cleanVersion } from './utils/version'; export function existingInstall(settings, logger) { try { diff --git a/src/cli_plugin/install/settings.js b/src/cli_plugin/install/settings.js index 94473cc12aab..e1536d66e052 100644 --- a/src/cli_plugin/install/settings.js +++ b/src/cli_plugin/install/settings.js @@ -7,10 +7,8 @@ */ import { resolve } from 'path'; - import expiry from 'expiry-js'; - -import { fromRoot } from '../../core/server/utils'; +import { fromRoot } from '@kbn/utils'; function generateUrls({ version, plugin }) { return [ diff --git a/src/cli_plugin/install/settings.test.js b/src/cli_plugin/install/settings.test.js index f06fd7eca790..c7985763524e 100644 --- a/src/cli_plugin/install/settings.test.js +++ b/src/cli_plugin/install/settings.test.js @@ -7,8 +7,8 @@ */ import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { fromRoot } from '@kbn/utils'; -import { fromRoot } from '../../core/server/utils'; import { parseMilliseconds, parse } from './settings'; const SECOND = 1000; diff --git a/src/legacy/utils/version.js b/src/cli_plugin/install/utils/version.js similarity index 100% rename from src/legacy/utils/version.js rename to src/cli_plugin/install/utils/version.js diff --git a/src/cli_plugin/list/index.js b/src/cli_plugin/list/index.js index ce55b939b8a4..02d1ed19f844 100644 --- a/src/cli_plugin/list/index.js +++ b/src/cli_plugin/list/index.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { fromRoot } from '../../core/server/utils'; +import { fromRoot } from '@kbn/utils'; import { list } from './list'; import { Logger } from '../lib/logger'; import { logWarnings } from '../lib/log_warnings'; diff --git a/src/cli_plugin/remove/settings.js b/src/cli_plugin/remove/settings.js index 333fa7cb0f2e..2381770ee0a6 100644 --- a/src/cli_plugin/remove/settings.js +++ b/src/cli_plugin/remove/settings.js @@ -7,8 +7,7 @@ */ import { resolve } from 'path'; - -import { fromRoot } from '../../core/server/utils'; +import { fromRoot } from '@kbn/utils'; export function parse(command, options) { const settings = { diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index a946640f58b0..b179c998f112 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -216,6 +216,7 @@ export class DocLinksService { }, maps: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, + importGeospatialPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/import-geospatial-data.html#import-geospatial-privileges`, }, monitoring: { alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index de13785a17f5..ed2d9bc0b391 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -11,6 +11,16 @@ min-height: 100%; } +#app-fixed-viewport { + pointer-events: none; + visibility: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + .app-wrapper { display: flex; flex-flow: column nowrap; @@ -35,6 +45,10 @@ @mixin kbnAffordForHeader($headerHeight) { padding-top: $headerHeight; + #app-fixed-viewport { + top: $headerHeight; + } + .euiFlyout, .euiCollapsibleNav { top: $headerHeight; diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx index 843f2a253f33..787fa475c7d5 100644 --- a/src/core/public/rendering/rendering_service.tsx +++ b/src/core/public/rendering/rendering_service.tsx @@ -52,6 +52,7 @@ export class RenderingService { {chromeHeader}
+
{bannerComponent}
{appComponent}
diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index 4a07e0c01068..a2267635e86f 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -83,6 +83,11 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot try { await root.setup(); await root.start(); + + // notify parent process know when we are ready for dev mode. + if (process.send) { + process.send(['SERVER_LISTENING']); + } } catch (err) { await shutdown(err); } diff --git a/src/core/server/config/ensure_valid_configuration.test.ts b/src/core/server/config/ensure_valid_configuration.test.ts new file mode 100644 index 000000000000..474e8dd59b4c --- /dev/null +++ b/src/core/server/config/ensure_valid_configuration.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { configServiceMock } from './mocks'; +import { ensureValidConfiguration } from './ensure_valid_configuration'; +import { CriticalError } from '../errors'; + +describe('ensureValidConfiguration', () => { + let configService: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + configService = configServiceMock.create(); + configService.getUsedPaths.mockReturnValue(Promise.resolve(['core', 'elastic'])); + }); + + it('returns normally when there is no unused keys', async () => { + configService.getUnusedPaths.mockResolvedValue([]); + await expect(ensureValidConfiguration(configService as any)).resolves.toBeUndefined(); + }); + + it('throws when there are some unused keys', async () => { + configService.getUnusedPaths.mockResolvedValue(['some.key', 'some.other.key']); + + await expect(ensureValidConfiguration(configService as any)).rejects.toMatchInlineSnapshot( + `[Error: Unknown configuration key(s): "some.key", "some.other.key". Check for spelling errors and ensure that expected plugins are installed.]` + ); + }); + + it('throws a `CriticalError` with the correct processExitCode value', async () => { + expect.assertions(2); + + configService.getUnusedPaths.mockResolvedValue(['some.key', 'some.other.key']); + + try { + await ensureValidConfiguration(configService as any); + } catch (e) { + expect(e).toBeInstanceOf(CriticalError); + expect(e.processExitCode).toEqual(64); + } + }); +}); diff --git a/src/core/server/legacy/config/ensure_valid_configuration.ts b/src/core/server/config/ensure_valid_configuration.ts similarity index 62% rename from src/core/server/legacy/config/ensure_valid_configuration.ts rename to src/core/server/config/ensure_valid_configuration.ts index fd3dd29e3d35..a33625cc0841 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.ts +++ b/src/core/server/config/ensure_valid_configuration.ts @@ -6,20 +6,13 @@ * Side Public License, v 1. */ -import { getUnusedConfigKeys } from './get_unused_config_keys'; -import { ConfigService } from '../../config'; -import { CriticalError } from '../../errors'; -import { LegacyServiceSetupConfig } from '../types'; +import { ConfigService } from '@kbn/config'; +import { CriticalError } from '../errors'; -export async function ensureValidConfiguration( - configService: ConfigService, - { legacyConfig, settings }: LegacyServiceSetupConfig -) { - const unusedConfigKeys = await getUnusedConfigKeys({ - coreHandledConfigPaths: await configService.getUsedPaths(), - settings, - legacyConfig, - }); +export async function ensureValidConfiguration(configService: ConfigService) { + await configService.validate(); + + const unusedConfigKeys = await configService.getUnusedPaths(); if (unusedConfigKeys.length > 0) { const message = `Unknown configuration key(s): ${unusedConfigKeys diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index b1086d447033..686564c6d678 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -7,6 +7,7 @@ */ export { coreDeprecationProvider } from './deprecation'; +export { ensureValidConfiguration } from './ensure_valid_configuration'; export { ConfigService, diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts index d51c36914695..830f4a9a9436 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts @@ -10,7 +10,7 @@ import { registerRouteForBundleMock } from './register_bundle_routes.test.mocks' import { PackageInfo } from '@kbn/config'; import { httpServiceMock } from '../../http/http_service.mock'; -import { UiPlugins } from '../../plugins'; +import { InternalPluginInfo, UiPlugins } from '../../plugins'; import { registerBundleRoutes } from './register_bundle_routes'; import { FileHashCache } from './file_hash_cache'; @@ -29,9 +29,12 @@ const createUiPlugins = (...ids: string[]): UiPlugins => ({ internal: ids.reduce((map, id) => { map.set(id, { publicTargetDir: `/plugins/${id}/public-target-dir`, + publicAssetsDir: `/plugins/${id}/public-assets-dir`, + version: '8.0.0', + requiredBundles: [], }); return map; - }, new Map()), + }, new Map()), }); describe('registerBundleRoutes', () => { @@ -86,16 +89,16 @@ describe('registerBundleRoutes', () => { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-a/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-a/', - routePath: '/42/bundles/plugin/plugin-a/', + publicPath: '/server-base-path/42/bundles/plugin/plugin-a/8.0.0/', + routePath: '/42/bundles/plugin/plugin-a/8.0.0/', }); expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-b/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-b/', - routePath: '/42/bundles/plugin/plugin-b/', + publicPath: '/server-base-path/42/bundles/plugin/plugin-b/8.0.0/', + routePath: '/42/bundles/plugin/plugin-b/8.0.0/', }); }); }); diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts index ee54f8ef3462..f313f1000363 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts @@ -8,10 +8,10 @@ import { join } from 'path'; import { PackageInfo } from '@kbn/config'; +import { fromRoot } from '@kbn/utils'; import { distDir as uiSharedDepsDistDir } from '@kbn/ui-shared-deps'; import { IRouter } from '../../http'; import { UiPlugins } from '../../plugins'; -import { fromRoot } from '../../utils'; import { FileHashCache } from './file_hash_cache'; import { registerRouteForBundle } from './bundles_route'; @@ -27,7 +27,7 @@ import { registerRouteForBundle } from './bundles_route'; */ export function registerBundleRoutes({ router, - serverBasePath, // serverBasePath + serverBasePath, uiPlugins, packageInfo, }: { @@ -57,10 +57,10 @@ export function registerBundleRoutes({ isDist, }); - [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir }]) => { + [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir, version }]) => { registerRouteForBundle(router, { - publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/`, - routePath: `/${buildNum}/bundles/plugin/${id}/`, + publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/${version}/`, + routePath: `/${buildNum}/bundles/plugin/${id}/${version}/`, bundlesPath: publicTargetDir, fileHashCache, isDist, diff --git a/src/core/server/core_app/core_app.ts b/src/core/server/core_app/core_app.ts index dac941767ebb..bc1098832bac 100644 --- a/src/core/server/core_app/core_app.ts +++ b/src/core/server/core_app/core_app.ts @@ -7,9 +7,11 @@ */ import Path from 'path'; +import { stringify } from 'querystring'; import { Env } from '@kbn/config'; +import { schema } from '@kbn/config-schema'; +import { fromRoot } from '@kbn/utils'; -import { fromRoot } from '../utils'; import { InternalCoreSetup } from '../internal_types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; @@ -49,6 +51,41 @@ export class CoreApp { }); }); + // remove trailing slash catch-all + router.get( + { + path: '/{path*}', + validate: { + params: schema.object({ + path: schema.maybe(schema.string()), + }), + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), + }, + }, + async (context, req, res) => { + const { query, params } = req; + const { path } = params; + if (!path || !path.endsWith('/')) { + return res.notFound(); + } + + const basePath = httpSetup.basePath.get(req); + let rewrittenPath = path.slice(0, -1); + if (`/${path}`.startsWith(basePath)) { + rewrittenPath = rewrittenPath.substring(basePath.length); + } + + const querystring = query ? stringify(query) : undefined; + const url = `${basePath}/${rewrittenPath}${querystring ? `?${querystring}` : ''}`; + + return res.redirected({ + headers: { + location: url, + }, + }); + } + ); + router.get({ path: '/core', validate: false }, async (context, req, res) => res.ok({ body: { version: '0.0.1' } }) ); diff --git a/src/core/server/core_app/integration_tests/core_app_routes.test.ts b/src/core/server/core_app/integration_tests/core_app_routes.test.ts new file mode 100644 index 000000000000..6b0643f7d1bc --- /dev/null +++ b/src/core/server/core_app/integration_tests/core_app_routes.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as kbnTestServer from '../../../test_helpers/kbn_server'; +import { Root } from '../../root'; + +describe('Core app routes', () => { + let root: Root; + + beforeAll(async function () { + root = kbnTestServer.createRoot({ + plugins: { initialize: false }, + server: { + basePath: '/base-path', + }, + }); + + await root.setup(); + await root.start(); + }); + + afterAll(async function () { + await root.shutdown(); + }); + + describe('`/{path*}` route', () => { + it('redirects requests to include the basePath', async () => { + const response = await kbnTestServer.request.get(root, '/some-path/').expect(302); + expect(response.get('location')).toEqual('/base-path/some-path'); + }); + + it('includes the query in the redirect', async () => { + const response = await kbnTestServer.request.get(root, '/some-path/?foo=bar').expect(302); + expect(response.get('location')).toEqual('/base-path/some-path?foo=bar'); + }); + + it('does not redirect if the path does not end with `/`', async () => { + await kbnTestServer.request.get(root, '/some-path').expect(404); + }); + + it('does not add the basePath if the path already contains it', async () => { + const response = await kbnTestServer.request.get(root, '/base-path/foo/').expect(302); + expect(response.get('location')).toEqual('/base-path/foo'); + }); + }); + + describe('`/` route', () => { + it('prevails on the `/{path*}` route', async () => { + const response = await kbnTestServer.request.get(root, '/').expect(302); + expect(response.get('location')).toEqual('/base-path/app/home'); + }); + }); +}); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 356dad201ce9..daf7424b8f8b 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -11,6 +11,7 @@ import { IHttpConfig, SslConfig, sslSchema } from '@kbn/server-http-tools'; import { hostname } from 'os'; import url from 'url'; +import { ServiceConfigDescriptor } from '../internal_types'; import { CspConfigType, CspConfig, ICspConfig } from '../csp'; import { ExternalUrlConfig, IExternalUrlConfig } from '../external_url'; @@ -20,141 +21,143 @@ const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); const match = (regex: RegExp, errorMsg: string) => (str: string) => regex.test(str) ? undefined : errorMsg; -// before update to make sure it's in sync with validation rules in Legacy -// https://github.com/elastic/kibana/blob/master/src/legacy/server/config/schema.js -export const config = { - path: 'server' as const, - schema: schema.object( - { - name: schema.string({ defaultValue: () => hostname() }), - autoListen: schema.boolean({ defaultValue: true }), - publicBaseUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), - basePath: schema.maybe( - schema.string({ - validate: match(validBasePathRegex, "must start with a slash, don't end with one"), - }) - ), - cors: schema.object( - { - enabled: schema.boolean({ defaultValue: false }), - allowCredentials: schema.boolean({ defaultValue: false }), - allowOrigin: schema.oneOf( - [ - schema.arrayOf(hostURISchema, { minSize: 1 }), - schema.arrayOf(schema.literal('*'), { minSize: 1, maxSize: 1 }), - ], - { - defaultValue: ['*'], - } - ), +const configSchema = schema.object( + { + name: schema.string({ defaultValue: () => hostname() }), + autoListen: schema.boolean({ defaultValue: true }), + publicBaseUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), + basePath: schema.maybe( + schema.string({ + validate: match(validBasePathRegex, "must start with a slash, don't end with one"), + }) + ), + cors: schema.object( + { + enabled: schema.boolean({ defaultValue: false }), + allowCredentials: schema.boolean({ defaultValue: false }), + allowOrigin: schema.oneOf( + [ + schema.arrayOf(hostURISchema, { minSize: 1 }), + schema.arrayOf(schema.literal('*'), { minSize: 1, maxSize: 1 }), + ], + { + defaultValue: ['*'], + } + ), + }, + { + validate(value) { + if (value.allowCredentials === true && value.allowOrigin.includes('*')) { + return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.'; + } }, - { - validate(value) { - if (value.allowCredentials === true && value.allowOrigin.includes('*')) { - return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.'; - } - }, + } + ), + customResponseHeaders: schema.recordOf(schema.string(), schema.any(), { + defaultValue: {}, + }), + host: schema.string({ + defaultValue: 'localhost', + hostname: true, + validate(value) { + if (value === '0') { + return 'value 0 is not a valid hostname (use "0.0.0.0" to bind to all interfaces)'; } + }, + }), + maxPayload: schema.byteSize({ + defaultValue: '1048576b', + }), + port: schema.number({ + defaultValue: 5601, + }), + rewriteBasePath: schema.boolean({ defaultValue: false }), + ssl: sslSchema, + keepaliveTimeout: schema.number({ + defaultValue: 120000, + }), + socketTimeout: schema.number({ + defaultValue: 120000, + }), + compression: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + referrerWhitelist: schema.maybe( + schema.arrayOf( + schema.string({ + hostname: true, + }), + { minSize: 1 } + ) + ), + }), + uuid: schema.maybe( + schema.string({ + validate: match(uuidRegexp, 'must be a valid uuid'), + }) + ), + xsrf: schema.object({ + disableProtection: schema.boolean({ defaultValue: false }), + allowlist: schema.arrayOf( + schema.string({ validate: match(/^\//, 'must start with a slash') }), + { defaultValue: [] } ), - customResponseHeaders: schema.recordOf(schema.string(), schema.any(), { - defaultValue: {}, - }), - host: schema.string({ - defaultValue: 'localhost', - hostname: true, + }), + requestId: schema.object( + { + allowFromAnyIp: schema.boolean({ defaultValue: false }), + ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }), + }, + { validate(value) { - if (value === '0') { - return 'value 0 is not a valid hostname (use "0.0.0.0" to bind to all interfaces)'; + if (value.allowFromAnyIp === true && value.ipAllowlist?.length > 0) { + return `allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist`; } }, - }), - maxPayload: schema.byteSize({ - defaultValue: '1048576b', - }), - port: schema.number({ - defaultValue: 5601, - }), - rewriteBasePath: schema.boolean({ defaultValue: false }), - ssl: sslSchema, - keepaliveTimeout: schema.number({ - defaultValue: 120000, - }), - socketTimeout: schema.number({ - defaultValue: 120000, - }), - compression: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - referrerWhitelist: schema.maybe( - schema.arrayOf( - schema.string({ - hostname: true, - }), - { minSize: 1 } - ) - ), - }), - uuid: schema.maybe( - schema.string({ - validate: match(uuidRegexp, 'must be a valid uuid'), - }) - ), - xsrf: schema.object({ - disableProtection: schema.boolean({ defaultValue: false }), - allowlist: schema.arrayOf( - schema.string({ validate: match(/^\//, 'must start with a slash') }), - { defaultValue: [] } - ), - }), - requestId: schema.object( - { - allowFromAnyIp: schema.boolean({ defaultValue: false }), - ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }), - }, - { - validate(value) { - if (value.allowFromAnyIp === true && value.ipAllowlist?.length > 0) { - return `allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist`; - } - }, + } + ), + }, + { + validate: (rawConfig) => { + if (!rawConfig.basePath && rawConfig.rewriteBasePath) { + return 'cannot use [rewriteBasePath] when [basePath] is not specified'; + } + + if (rawConfig.publicBaseUrl) { + const parsedUrl = url.parse(rawConfig.publicBaseUrl); + if (parsedUrl.query || parsedUrl.hash || parsedUrl.auth) { + return `[publicBaseUrl] may only contain a protocol, host, port, and pathname`; } - ), - }, - { - validate: (rawConfig) => { - if (!rawConfig.basePath && rawConfig.rewriteBasePath) { - return 'cannot use [rewriteBasePath] when [basePath] is not specified'; + if (parsedUrl.path !== (rawConfig.basePath ?? '/')) { + return `[publicBaseUrl] must contain the [basePath]: ${parsedUrl.path} !== ${rawConfig.basePath}`; } + } - if (rawConfig.publicBaseUrl) { - const parsedUrl = url.parse(rawConfig.publicBaseUrl); - if (parsedUrl.query || parsedUrl.hash || parsedUrl.auth) { - return `[publicBaseUrl] may only contain a protocol, host, port, and pathname`; - } - if (parsedUrl.path !== (rawConfig.basePath ?? '/')) { - return `[publicBaseUrl] must contain the [basePath]: ${parsedUrl.path} !== ${rawConfig.basePath}`; - } - } + if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) { + return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false'; + } - if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) { - return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false'; - } + if ( + rawConfig.ssl.enabled && + rawConfig.ssl.redirectHttpFromPort !== undefined && + rawConfig.ssl.redirectHttpFromPort === rawConfig.port + ) { + return ( + 'Kibana does not accept http traffic to [port] when ssl is ' + + 'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' + + `cannot be configured to the same value. Both are [${rawConfig.port}].` + ); + } + }, + } +); - if ( - rawConfig.ssl.enabled && - rawConfig.ssl.redirectHttpFromPort !== undefined && - rawConfig.ssl.redirectHttpFromPort === rawConfig.port - ) { - return ( - 'Kibana does not accept http traffic to [port] when ssl is ' + - 'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' + - `cannot be configured to the same value. Both are [${rawConfig.port}].` - ); - } - }, - } - ), +export type HttpConfigType = TypeOf; + +export const config: ServiceConfigDescriptor = { + path: 'server' as const, + schema: configSchema, + deprecations: ({ rename }) => [rename('maxPayloadBytes', 'maxPayload')], }; -export type HttpConfigType = TypeOf; export class HttpConfig implements IHttpConfig { public name: string; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index af358caae8bf..5433f0d3c3e3 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -12,8 +12,6 @@ import { legacyClusterClientInstanceMock, } from './core_service.test.mocks'; -import Boom from '@hapi/boom'; -import { Request } from '@hapi/hapi'; import { errors as esErrors } from 'elasticsearch'; import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy'; @@ -22,16 +20,6 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { InternalElasticsearchServiceStart } from '../../elasticsearch'; -interface User { - id: string; - roles?: string[]; -} - -interface StorageData { - value: User; - expires: number; -} - const cookieOptions = { name: 'sid', encryptionKey: 'something_at_least_32_characters', @@ -197,172 +185,6 @@ describe('http service', () => { }); }); - describe('legacy server', () => { - describe('#registerAuth()', () => { - const sessionDurationMs = 1000; - - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot({ plugins: { initialize: false } }); - }, 30000); - - afterEach(async () => { - MockLegacyScopedClusterClient.mockClear(); - await root.shutdown(); - }); - - it('runs auth for legacy routes and proxy request to legacy server route handlers', async () => { - const { http } = await root.setup(); - const sessionStorageFactory = await http.createCookieSessionStorageFactory( - cookieOptions - ); - http.registerAuth((req, res, toolkit) => { - if (req.headers.authorization) { - const user = { id: '42' }; - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return toolkit.authenticated({ state: user }); - } else { - return res.unauthorized(); - } - }); - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: () => 'ok from legacy server', - }); - - const response = await kbnTestServer.request - .get(root, legacyUrl) - .expect(200, 'ok from legacy server'); - - expect(response.header['set-cookie']).toHaveLength(1); - }); - - it('passes authHeaders as request headers to the legacy platform', async () => { - const token = 'Basic: name:password'; - const { http } = await root.setup(); - const sessionStorageFactory = await http.createCookieSessionStorageFactory( - cookieOptions - ); - http.registerAuth((req, res, toolkit) => { - if (req.headers.authorization) { - const user = { id: '42' }; - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return toolkit.authenticated({ - state: user, - requestHeaders: { - authorization: token, - }, - }); - } else { - return res.unauthorized(); - } - }); - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: (req: Request) => ({ - authorization: req.headers.authorization, - custom: req.headers.custom, - }), - }); - - await kbnTestServer.request - .get(root, legacyUrl) - .set({ custom: 'custom-header' }) - .expect(200, { authorization: token, custom: 'custom-header' }); - }); - - it('attach security header to a successful response handled by Legacy platform', async () => { - const authResponseHeader = { - 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', - }; - const { http } = await root.setup(); - const { registerAuth } = http; - - registerAuth((req, res, toolkit) => { - return toolkit.authenticated({ responseHeaders: authResponseHeader }); - }); - - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: '/legacy', - handler: () => 'ok', - }); - - const response = await kbnTestServer.request.get(root, '/legacy').expect(200); - expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); - }); - - it('attach security header to an error response handled by Legacy platform', async () => { - const authResponseHeader = { - 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', - }; - const { http } = await root.setup(); - const { registerAuth } = http; - - registerAuth((req, res, toolkit) => { - return toolkit.authenticated({ responseHeaders: authResponseHeader }); - }); - - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: '/legacy', - handler: () => { - throw Boom.badRequest(); - }, - }); - - const response = await kbnTestServer.request.get(root, '/legacy').expect(400); - expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); - }); - }); - - describe('#basePath()', () => { - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot({ plugins: { initialize: false } }); - }, 30000); - - afterEach(async () => await root.shutdown()); - it('basePath information for an incoming request is available in legacy server', async () => { - const reqBasePath = '/requests-specific-base-path'; - const { http } = await root.setup(); - http.registerOnPreRouting((req, res, toolkit) => { - http.basePath.set(req, reqBasePath); - return toolkit.next(); - }); - - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: kbnServer.newPlatform.setup.core.http.basePath.get, - }); - - await kbnTestServer.request.get(root, legacyUrl).expect(200, reqBasePath); - }); - }); - }); describe('legacy elasticsearch client', () => { let root: ReturnType; beforeEach(async () => { diff --git a/src/core/server/i18n/get_kibana_translation_files.test.ts b/src/core/server/i18n/get_kibana_translation_files.test.ts index 7ca0fe0e7933..45e1a8dfec9c 100644 --- a/src/core/server/i18n/get_kibana_translation_files.test.ts +++ b/src/core/server/i18n/get_kibana_translation_files.test.ts @@ -14,7 +14,7 @@ const mockGetTranslationPaths = getTranslationPaths as jest.Mock; jest.mock('./get_translation_paths', () => ({ getTranslationPaths: jest.fn().mockResolvedValue([]), })); -jest.mock('../utils', () => ({ +jest.mock('@kbn/utils', () => ({ fromRoot: jest.fn().mockImplementation((path: string) => path), })); diff --git a/src/core/server/i18n/get_kibana_translation_files.ts b/src/core/server/i18n/get_kibana_translation_files.ts index 7b5ada2a25f4..4e7ee718113c 100644 --- a/src/core/server/i18n/get_kibana_translation_files.ts +++ b/src/core/server/i18n/get_kibana_translation_files.ts @@ -7,7 +7,7 @@ */ import { basename } from 'path'; -import { fromRoot } from '../utils'; +import { fromRoot } from '@kbn/utils'; import { getTranslationPaths } from './get_translation_paths'; export const getKibanaTranslationFiles = async ( diff --git a/src/core/server/i18n/get_translation_paths.test.ts b/src/core/server/i18n/get_translation_paths.test.ts index 9094b008be73..3e9d68c16d30 100644 --- a/src/core/server/i18n/get_translation_paths.test.ts +++ b/src/core/server/i18n/get_translation_paths.test.ts @@ -23,14 +23,14 @@ describe('getTranslationPaths', () => { getTranslationPaths({ cwd: '/some/cwd', nested: false }); expect(globbyMock).toHaveBeenCalledTimes(1); - expect(globbyMock).toHaveBeenCalledWith('.i18nrc.json', { cwd: '/some/cwd' }); + expect(globbyMock).toHaveBeenCalledWith('.i18nrc.json', { cwd: '/some/cwd', dot: true }); globbyMock.mockClear(); await getTranslationPaths({ cwd: '/other/cwd', nested: true }); expect(globbyMock).toHaveBeenCalledTimes(1); - expect(globbyMock).toHaveBeenCalledWith('*/.i18nrc.json', { cwd: '/other/cwd' }); + expect(globbyMock).toHaveBeenCalledWith('*/.i18nrc.json', { cwd: '/other/cwd', dot: true }); }); it('calls `readFile` for each entry returned by `globby`', async () => { diff --git a/src/core/server/i18n/get_translation_paths.ts b/src/core/server/i18n/get_translation_paths.ts index 93b10da73dcc..8897786252d4 100644 --- a/src/core/server/i18n/get_translation_paths.ts +++ b/src/core/server/i18n/get_translation_paths.ts @@ -18,7 +18,7 @@ const I18N_RC = '.i18nrc.json'; export async function getTranslationPaths({ cwd, nested }: { cwd: string; nested: boolean }) { const glob = nested ? `*/${I18N_RC}` : I18N_RC; - const entries = await globby(glob, { cwd }); + const entries = await globby(glob, { cwd, dot: true }); const translationPaths: string[] = []; for (const entry of entries) { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 963b69eac4f7..2c6fa74cb54a 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -406,8 +406,6 @@ export type { SavedObjectsMigrationVersion, } from './types'; -export type { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig } from './legacy'; - export { ServiceStatusLevels } from './status'; export type { CoreStatus, ServiceStatus, ServiceStatusLevel, StatusServiceSetup } from './status'; diff --git a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap deleted file mode 100644 index 69b7f9fc7831..000000000000 --- a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`once LegacyService is set up with connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` -Array [ - Array [ - Object { - "logging": Object { - "verbose": true, - }, - "path": Object {}, - }, - ], -] -`; - -exports[`once LegacyService is set up without connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` -Array [ - Array [ - Object { - "logging": Object { - "verbose": true, - }, - "path": Object {}, - }, - ], -] -`; diff --git a/src/core/server/legacy/config/ensure_valid_configuration.test.ts b/src/core/server/legacy/config/ensure_valid_configuration.test.ts deleted file mode 100644 index febf91625378..000000000000 --- a/src/core/server/legacy/config/ensure_valid_configuration.test.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ensureValidConfiguration } from './ensure_valid_configuration'; -import { getUnusedConfigKeys } from './get_unused_config_keys'; -import { configServiceMock } from '../../config/mocks'; - -jest.mock('./get_unused_config_keys'); - -describe('ensureValidConfiguration', () => { - let configService: ReturnType; - - beforeEach(() => { - jest.clearAllMocks(); - configService = configServiceMock.create(); - configService.getUsedPaths.mockReturnValue(Promise.resolve(['core', 'elastic'])); - - (getUnusedConfigKeys as any).mockImplementation(() => []); - }); - - it('calls getUnusedConfigKeys with correct parameters', async () => { - await ensureValidConfiguration( - configService as any, - { - settings: 'settings', - legacyConfig: 'pluginExtendedConfig', - } as any - ); - expect(getUnusedConfigKeys).toHaveBeenCalledTimes(1); - expect(getUnusedConfigKeys).toHaveBeenCalledWith({ - coreHandledConfigPaths: ['core', 'elastic'], - settings: 'settings', - legacyConfig: 'pluginExtendedConfig', - }); - }); - - it('returns normally when there is no unused keys', async () => { - await expect( - ensureValidConfiguration(configService as any, {} as any) - ).resolves.toBeUndefined(); - - expect(getUnusedConfigKeys).toHaveBeenCalledTimes(1); - }); - - it('throws when there are some unused keys', async () => { - (getUnusedConfigKeys as any).mockImplementation(() => ['some.key', 'some.other.key']); - - await expect( - ensureValidConfiguration(configService as any, {} as any) - ).rejects.toMatchInlineSnapshot( - `[Error: Unknown configuration key(s): "some.key", "some.other.key". Check for spelling errors and ensure that expected plugins are installed.]` - ); - }); -}); diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts deleted file mode 100644 index 86b4e0aeeea5..000000000000 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LegacyConfig, LegacyVars } from '../types'; -import { getUnusedConfigKeys } from './get_unused_config_keys'; - -describe('getUnusedConfigKeys', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - const getConfig = (values: LegacyVars = {}): LegacyConfig => - ({ - get: () => values as any, - } as LegacyConfig); - - describe('not using core or plugin specs', () => { - it('should return an empty list for empty parameters', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: {}, - legacyConfig: getConfig(), - }) - ).toEqual([]); - }); - - it('returns empty list when config and settings have the same properties', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - presentInBoth: true, - alsoInBoth: 'someValue', - }, - legacyConfig: getConfig({ - presentInBoth: true, - alsoInBoth: 'someValue', - }), - }) - ).toEqual([]); - }); - - it('returns empty list when config has entries not present in settings', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - presentInBoth: true, - }, - legacyConfig: getConfig({ - presentInBoth: true, - onlyInConfig: 'someValue', - }), - }) - ).toEqual([]); - }); - - it('returns the list of properties from settings not present in config', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - presentInBoth: true, - onlyInSetting: 'value', - }, - legacyConfig: getConfig({ - presentInBoth: true, - }), - }) - ).toEqual(['onlyInSetting']); - }); - - it('correctly handle nested properties', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - elasticsearch: { - username: 'foo', - password: 'bar', - }, - }, - legacyConfig: getConfig({ - elasticsearch: { - username: 'foo', - onlyInConfig: 'default', - }, - }), - }) - ).toEqual(['elasticsearch.password']); - }); - - it('correctly handle "env" specific case', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - env: 'development', - }, - legacyConfig: getConfig({ - env: { - name: 'development', - }, - }), - }) - ).toEqual([]); - }); - - it('correctly handle array properties', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - prop: ['a', 'b', 'c'], - }, - legacyConfig: getConfig({ - prop: ['a'], - }), - }) - ).toEqual([]); - }); - }); - - it('ignores properties managed by the new platform', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: ['core', 'foo.bar'], - settings: { - core: { - prop: 'value', - }, - foo: { - bar: true, - dolly: true, - }, - }, - legacyConfig: getConfig({}), - }) - ).toEqual(['foo.dolly']); - }); - - it('handles array values', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: ['core', 'array'], - settings: { - core: { - prop: 'value', - array: [1, 2, 3], - }, - array: ['some', 'values'], - }, - legacyConfig: getConfig({}), - }) - ).toEqual([]); - }); -}); diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts deleted file mode 100644 index a2da6dc97225..000000000000 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { difference } from 'lodash'; -import { getFlattenedObject } from '@kbn/std'; -import { hasConfigPathIntersection } from '../../config'; -import { LegacyConfig, LegacyVars } from '../types'; - -const getFlattenedKeys = (object: object) => Object.keys(getFlattenedObject(object)); - -export async function getUnusedConfigKeys({ - coreHandledConfigPaths, - settings, - legacyConfig, -}: { - coreHandledConfigPaths: string[]; - settings: LegacyVars; - legacyConfig: LegacyConfig; -}) { - const inputKeys = getFlattenedKeys(settings); - const appliedKeys = getFlattenedKeys(legacyConfig.get()); - - if (inputKeys.includes('env')) { - // env is a special case key, see https://github.com/elastic/kibana/blob/848bf17b/src/legacy/server/config/config.js#L74 - // where it is deleted from the settings before being injected into the schema via context and - // then renamed to `env.name` https://github.com/elastic/kibana/blob/848bf17/src/legacy/server/config/schema.js#L17 - inputKeys[inputKeys.indexOf('env')] = 'env.name'; - } - - // Filter out keys that are marked as used in the core (e.g. by new core plugins). - return difference(inputKeys, appliedKeys).filter( - (unusedConfigKey) => - !coreHandledConfigPaths.some((usedInCoreConfigKey) => - hasConfigPathIntersection(unusedConfigKey, usedInCoreConfigKey) - ) - ); -} diff --git a/src/core/server/legacy/index.ts b/src/core/server/legacy/index.ts index 8614265e4375..39ffef501a9e 100644 --- a/src/core/server/legacy/index.ts +++ b/src/core/server/legacy/index.ts @@ -6,16 +6,6 @@ * Side Public License, v 1. */ -/** @internal */ -export { ensureValidConfiguration } from './config'; /** @internal */ export type { ILegacyService } from './legacy_service'; export { LegacyService } from './legacy_service'; -/** @internal */ -export type { - LegacyVars, - LegacyConfig, - LegacyServiceSetupDeps, - LegacyServiceStartDeps, - LegacyServiceSetupConfig, -} from './types'; diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts deleted file mode 100644 index 715749c6ef0c..000000000000 --- a/src/core/server/legacy/integration_tests/legacy_service.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as kbnTestServer from '../../../test_helpers/kbn_server'; - -describe('legacy service', () => { - describe('http server', () => { - let root: ReturnType; - beforeEach(() => { - root = kbnTestServer.createRoot({ - migrations: { skip: true }, - plugins: { initialize: false }, - }); - }, 30000); - - afterEach(async () => await root.shutdown()); - - it("handles http request in Legacy platform if New platform doesn't handle it", async () => { - const { http } = await root.setup(); - const rootUrl = '/route'; - const router = http.createRouter(rootUrl); - router.get({ path: '/new-platform', validate: false }, (context, req, res) => - res.ok({ body: 'from-new-platform' }) - ); - - await root.start(); - - const legacyPlatformUrl = `${rootUrl}/legacy-platform`; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyPlatformUrl, - handler: () => 'ok from legacy server', - }); - - await kbnTestServer.request.get(root, '/route/new-platform').expect(200, 'from-new-platform'); - - await kbnTestServer.request.get(root, legacyPlatformUrl).expect(200, 'ok from legacy server'); - }); - it('throws error if Legacy and New platforms register handler for the same route', async () => { - const { http } = await root.setup(); - const rootUrl = '/route'; - const router = http.createRouter(rootUrl); - router.get({ path: '', validate: false }, (context, req, res) => - res.ok({ body: 'from-new-platform' }) - ); - - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - expect(() => - kbnServer.server.route({ - method: 'GET', - path: rootUrl, - handler: () => 'ok from legacy server', - }) - ).toThrowErrorMatchingInlineSnapshot(`"New route /route conflicts with existing /route"`); - }); - }); -}); diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts index 1f4c308be010..0d72318a630e 100644 --- a/src/core/server/legacy/legacy_service.mock.ts +++ b/src/core/server/legacy/legacy_service.mock.ts @@ -8,26 +8,14 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { LegacyService } from './legacy_service'; -import { LegacyConfig, LegacyServiceSetupDeps } from './types'; -type LegacyServiceMock = jest.Mocked & { legacyId: symbol }>; +type LegacyServiceMock = jest.Mocked>; const createLegacyServiceMock = (): LegacyServiceMock => ({ - legacyId: Symbol(), - setupLegacyConfig: jest.fn(), setup: jest.fn(), - start: jest.fn(), stop: jest.fn(), }); -const createLegacyConfigMock = (): jest.Mocked => ({ - get: jest.fn(), - has: jest.fn(), - set: jest.fn(), -}); - export const legacyServiceMock = { create: createLegacyServiceMock, - createSetupContract: (deps: LegacyServiceSetupDeps) => createLegacyServiceMock().setup(deps), - createLegacyConfig: createLegacyConfigMock, }; diff --git a/src/core/server/legacy/legacy_service.test.mocks.ts b/src/core/server/legacy/legacy_service.test.mocks.ts new file mode 100644 index 000000000000..506f0fd6f96d --- /dev/null +++ b/src/core/server/legacy/legacy_service.test.mocks.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const reconfigureLoggingMock = jest.fn(); +export const setupLoggingMock = jest.fn(); +export const setupLoggingRotateMock = jest.fn(); + +jest.doMock('@kbn/legacy-logging', () => ({ + ...(jest.requireActual('@kbn/legacy-logging') as any), + reconfigureLogging: reconfigureLoggingMock, + setupLogging: setupLoggingMock, + setupLoggingRotate: setupLoggingRotateMock, +})); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index d0a02b985996..6b20bd7434ba 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -6,35 +6,22 @@ * Side Public License, v 1. */ -jest.mock('../../../legacy/server/kbn_server'); - -import { BehaviorSubject, throwError } from 'rxjs'; +import { + setupLoggingMock, + setupLoggingRotateMock, + reconfigureLoggingMock, +} from './legacy_service.test.mocks'; + +import { BehaviorSubject } from 'rxjs'; +import moment from 'moment'; import { REPO_ROOT } from '@kbn/dev-utils'; -import KbnServer from '../../../legacy/server/kbn_server'; import { Config, Env, ObjectToConfigAdapter } from '../config'; -import { DiscoveredPlugin } from '../plugins'; import { getEnvOptions, configServiceMock } from '../config/mocks'; import { loggingSystemMock } from '../logging/logging_system.mock'; -import { contextServiceMock } from '../context/context_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; -import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; -import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; -import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; -import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; -import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; -import { environmentServiceMock } from '../environment/environment_service.mock'; -import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; -import { LegacyService } from './legacy_service'; -import { coreMock } from '../mocks'; -import { statusServiceMock } from '../status/status_service.mock'; -import { loggingServiceMock } from '../logging/logging_service.mock'; -import { metricsServiceMock } from '../metrics/metrics_service.mock'; -import { i18nServiceMock } from '../i18n/i18n_service.mock'; -import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock'; - -const MockKbnServer: jest.Mock = KbnServer as any; +import { LegacyService, LegacyServiceSetupDeps } from './legacy_service'; let coreId: symbol; let env: Env; @@ -42,70 +29,16 @@ let config$: BehaviorSubject; let setupDeps: LegacyServiceSetupDeps; -let startDeps: LegacyServiceStartDeps; - const logger = loggingSystemMock.create(); let configService: ReturnType; -let environmentSetup: ReturnType; beforeEach(() => { coreId = Symbol(); env = Env.createDefault(REPO_ROOT, getEnvOptions()); configService = configServiceMock.create(); - environmentSetup = environmentServiceMock.createSetupContract(); - - MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); - MockKbnServer.prototype.listen = jest.fn(); setupDeps = { - core: { - capabilities: capabilitiesServiceMock.createSetupContract(), - context: contextServiceMock.createSetupContract(), - elasticsearch: { legacy: {} } as any, - i18n: i18nServiceMock.createSetupContract(), - uiSettings: uiSettingsServiceMock.createSetupContract(), - http: { - ...httpServiceMock.createInternalSetupContract(), - auth: { - getAuthHeaders: () => undefined, - } as any, - }, - httpResources: httpResourcesMock.createSetupContract(), - savedObjects: savedObjectsServiceMock.createInternalSetupContract(), - plugins: { - initialized: true, - contracts: new Map([['plugin-id', 'plugin-value']]), - }, - rendering: renderingServiceMock, - environment: environmentSetup, - status: statusServiceMock.createInternalSetupContract(), - logging: loggingServiceMock.createInternalSetupContract(), - metrics: metricsServiceMock.createInternalSetupContract(), - deprecations: deprecationsServiceMock.createInternalSetupContract(), - }, - plugins: { 'plugin-id': 'plugin-value' }, - uiPlugins: { - public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([ - [ - 'plugin-id', - { - requiredBundles: [], - publicTargetDir: 'path/to/target/public', - publicAssetsDir: '/plugins/name/assets/', - }, - ], - ]), - browserConfigs: new Map(), - }, - }; - - startDeps = { - core: { - ...coreMock.createInternalStart(), - plugins: { contracts: new Map() }, - }, - plugins: {}, + http: httpServiceMock.createInternalSetupContract(), }; config$ = new BehaviorSubject( @@ -116,98 +49,78 @@ beforeEach(() => { ); configService.getConfig$.mockReturnValue(config$); - configService.getUsedPaths.mockResolvedValue(['foo.bar']); }); afterEach(() => { jest.clearAllMocks(); + setupLoggingMock.mockReset(); + setupLoggingRotateMock.mockReset(); + reconfigureLoggingMock.mockReset(); }); -describe('once LegacyService is set up with connection info', () => { - test('creates legacy kbnServer and calls `listen`.', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService, +describe('#setup', () => { + it('initializes legacy logging', async () => { + const opsConfig = { + interval: moment.duration(5, 'second'), + }; + const opsConfig$ = new BehaviorSubject(opsConfig); + + const loggingConfig = { + foo: 'bar', + }; + const loggingConfig$ = new BehaviorSubject(loggingConfig); + + configService.atPath.mockImplementation((path) => { + if (path === 'ops') { + return opsConfig$; + } + if (path === 'logging') { + return loggingConfig$; + } + return new BehaviorSubject({}); }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - expect(MockKbnServer).toHaveBeenCalledTimes(1); - expect(MockKbnServer).toHaveBeenCalledWith( - { path: { autoListen: true }, server: { autoListen: true } }, // Because of the mock, path also gets the value - expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object) - ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual( - expect.objectContaining({ - path: expect.objectContaining({ autoListen: true }), - server: expect.objectContaining({ autoListen: true }), - }) - ); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.listen).toHaveBeenCalledTimes(1); - expect(mockKbnServer.close).not.toHaveBeenCalled(); - }); - - test('creates legacy kbnServer but does not call `listen` if `autoListen: false`.', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false })); - const legacyService = new LegacyService({ coreId, env, logger, configService: configService as any, }); - await legacyService.setupLegacyConfig(); + await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - expect(MockKbnServer).toHaveBeenCalledTimes(1); - expect(MockKbnServer).toHaveBeenCalledWith( - { path: { autoListen: false }, server: { autoListen: true } }, - expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object) + expect(setupLoggingMock).toHaveBeenCalledTimes(1); + expect(setupLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + loggingConfig, + opsConfig.interval.asMilliseconds() ); - const legacyConfig = MockKbnServer.mock.calls[0][1].get(); - expect(legacyConfig.path.autoListen).toBe(false); - expect(legacyConfig.server.autoListen).toBe(true); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.ready).toHaveBeenCalledTimes(1); - expect(mockKbnServer.listen).not.toHaveBeenCalled(); - expect(mockKbnServer.close).not.toHaveBeenCalled(); + expect(setupLoggingRotateMock).toHaveBeenCalledTimes(1); + expect(setupLoggingRotateMock).toHaveBeenCalledWith(setupDeps.http.server, loggingConfig); }); - test('creates legacy kbnServer and closes it if `listen` fails.', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed')); - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + it('reloads the logging config when the config changes', async () => { + const opsConfig = { + interval: moment.duration(5, 'second'), + }; + const opsConfig$ = new BehaviorSubject(opsConfig); + + const loggingConfig = { + foo: 'bar', + }; + const loggingConfig$ = new BehaviorSubject(loggingConfig); + + configService.atPath.mockImplementation((path) => { + if (path === 'ops') { + return opsConfig$; + } + if (path === 'logging') { + return loggingConfig$; + } + return new BehaviorSubject({}); }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"something failed"` - ); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.listen).toHaveBeenCalled(); - expect(mockKbnServer.close).toHaveBeenCalled(); - }); - - test('throws if fails to retrieve initial config.', async () => { - configService.getConfig$.mockReturnValue(throwError(new Error('something failed'))); const legacyService = new LegacyService({ coreId, env, @@ -215,150 +128,70 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); - await expect(legacyService.setupLegacyConfig()).rejects.toThrowErrorMatchingInlineSnapshot( - `"something failed"` - ); - await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()"` - ); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy service is not setup yet."` - ); - - expect(MockKbnServer).not.toHaveBeenCalled(); - }); - - test('reconfigures logging configuration if new config is received.', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, - }); - await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - const [mockKbnServer] = MockKbnServer.mock.instances as Array>; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - - config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); - expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot( - `applyLoggingConfiguration params` + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); + expect(reconfigureLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + loggingConfig, + opsConfig.interval.asMilliseconds() ); - }); - test('logs error if re-configuring fails.', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + loggingConfig$.next({ + foo: 'changed', }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - const [mockKbnServer] = MockKbnServer.mock.instances as Array>; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - expect(loggingSystemMock.collect(logger).error).toEqual([]); + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(2); + expect(reconfigureLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + { foo: 'changed' }, + opsConfig.interval.asMilliseconds() + ); + }); - const configError = new Error('something went wrong'); - mockKbnServer.applyLoggingConfiguration.mockImplementation(() => { - throw configError; + it('stops reloading logging config once the service is stopped', async () => { + const opsConfig = { + interval: moment.duration(5, 'second'), + }; + const opsConfig$ = new BehaviorSubject(opsConfig); + + const loggingConfig = { + foo: 'bar', + }; + const loggingConfig$ = new BehaviorSubject(loggingConfig); + + configService.atPath.mockImplementation((path) => { + if (path === 'ops') { + return opsConfig$; + } + if (path === 'logging') { + return loggingConfig$; + } + return new BehaviorSubject({}); }); - config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); - - expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]); - }); - - test('logs error if config service fails.', async () => { const legacyService = new LegacyService({ coreId, env, logger, configService: configService as any, }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - expect(loggingSystemMock.collect(logger).error).toEqual([]); - - const configError = new Error('something went wrong'); - config$.error(configError); - - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]); - }); -}); -describe('once LegacyService is set up without connection info', () => { - let legacyService: LegacyService; - beforeEach(async () => { - legacyService = new LegacyService({ coreId, env, logger, configService: configService as any }); - await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - }); - test('creates legacy kbnServer with `autoListen: false`.', () => { - expect(MockKbnServer).toHaveBeenCalledTimes(1); - expect(MockKbnServer).toHaveBeenCalledWith( - { path: {}, server: { autoListen: true } }, - expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object) - ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual( - expect.objectContaining({ - server: expect.objectContaining({ autoListen: true }), - }) + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); + expect(reconfigureLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + loggingConfig, + opsConfig.interval.asMilliseconds() ); - }); - - test('reconfigures logging configuration if new config is received.', async () => { - const [mockKbnServer] = MockKbnServer.mock.instances as Array>; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); + await legacyService.stop(); - expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot( - `applyLoggingConfiguration params` - ); - }); -}); - -describe('start', () => { - test('Cannot start without setup phase', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + loggingConfig$.next({ + foo: 'changed', }); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy service is not setup yet."` - ); - }); -}); -test('Sets the server.uuid property on the legacy configuration', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); }); - - environmentSetup.instanceUuid = 'UUID_FROM_SERVICE'; - - const { legacyConfig } = await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - - expect(legacyConfig.get('server.uuid')).toBe('UUID_FROM_SERVICE'); }); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 43b348a5ff4a..1d5343ff5311 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -6,141 +6,61 @@ * Side Public License, v 1. */ -import { combineLatest, ConnectableObservable, Observable, Subscription } from 'rxjs'; -import { first, map, publishReplay, tap } from 'rxjs/operators'; +import { combineLatest, Observable, Subscription } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { Server } from '@hapi/hapi'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { PathConfigType } from '@kbn/utils'; +import { + reconfigureLogging, + setupLogging, + setupLoggingRotate, + LegacyLoggingConfig, +} from '@kbn/legacy-logging'; -import type { RequestHandlerContext } from 'src/core/server'; -// @ts-expect-error legacy config class -import { Config as LegacyConfigClass } from '../../../legacy/server/config'; -import { CoreService } from '../../types'; -import { Config } from '../config'; import { CoreContext } from '../core_context'; -import { CspConfigType, config as cspConfig } from '../csp'; -import { - HttpConfig, - HttpConfigType, - config as httpConfig, - IRouter, - RequestHandlerContextProvider, -} from '../http'; +import { config as loggingConfig } from '../logging'; +import { opsConfig, OpsConfigType } from '../metrics'; import { Logger } from '../logging'; -import { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig, LegacyVars } from './types'; -import { ExternalUrlConfigType, config as externalUrlConfig } from '../external_url'; -import { CoreSetup, CoreStart } from '..'; - -interface LegacyKbnServer { - applyLoggingConfiguration: (settings: Readonly) => void; - listen: () => Promise; - ready: () => Promise; - close: () => Promise; -} +import { InternalHttpServiceSetup } from '../http'; -function getLegacyRawConfig(config: Config, pathConfig: PathConfigType) { - const rawConfig = config.toRaw(); - - // Elasticsearch config is solely handled by the core and legacy platform - // shouldn't have direct access to it. - if (rawConfig.elasticsearch !== undefined) { - delete rawConfig.elasticsearch; - } - - return { - ...rawConfig, - // We rely heavily in the default value of 'path.data' in the legacy world and, - // since it has been moved to NP, it won't show up in RawConfig. - path: pathConfig, - }; +export interface LegacyServiceSetupDeps { + http: InternalHttpServiceSetup; } /** @internal */ export type ILegacyService = PublicMethodsOf; /** @internal */ -export class LegacyService implements CoreService { - /** Symbol to represent the legacy platform as a fake "plugin". Used by the ContextService */ - public readonly legacyId = Symbol(); +export class LegacyService { private readonly log: Logger; - private readonly httpConfig$: Observable; - private kbnServer?: LegacyKbnServer; + private readonly opsConfig$: Observable; + private readonly legacyLoggingConfig$: Observable; private configSubscription?: Subscription; - private setupDeps?: LegacyServiceSetupDeps; - private update$?: ConnectableObservable<[Config, PathConfigType]>; - private legacyRawConfig?: LegacyConfig; - private settings?: LegacyVars; - constructor(private readonly coreContext: CoreContext) { + constructor(coreContext: CoreContext) { const { logger, configService } = coreContext; this.log = logger.get('legacy-service'); - this.httpConfig$ = combineLatest( - configService.atPath(httpConfig.path), - configService.atPath(cspConfig.path), - configService.atPath(externalUrlConfig.path) - ).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl))); - } - - public async setupLegacyConfig() { - this.update$ = combineLatest([ - this.coreContext.configService.getConfig$(), - this.coreContext.configService.atPath('path'), - ]).pipe( - tap(([config, pathConfig]) => { - if (this.kbnServer !== undefined) { - this.kbnServer.applyLoggingConfiguration(getLegacyRawConfig(config, pathConfig)); - } - }), - tap({ error: (err) => this.log.error(err) }), - publishReplay(1) - ) as ConnectableObservable<[Config, PathConfigType]>; - - this.configSubscription = this.update$.connect(); - - this.settings = await this.update$ - .pipe( - first(), - map(([config, pathConfig]) => getLegacyRawConfig(config, pathConfig)) - ) - .toPromise(); - - this.legacyRawConfig = LegacyConfigClass.withDefaultSchema(this.settings); - - return { - settings: this.settings, - legacyConfig: this.legacyRawConfig!, - }; + this.legacyLoggingConfig$ = configService.atPath(loggingConfig.path); + this.opsConfig$ = configService.atPath(opsConfig.path); } public async setup(setupDeps: LegacyServiceSetupDeps) { this.log.debug('setting up legacy service'); - - if (!this.legacyRawConfig) { - throw new Error( - 'Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()' - ); - } - - // propagate the instance uuid to the legacy config, as it was the legacy way to access it. - this.legacyRawConfig!.set('server.uuid', setupDeps.core.environment.instanceUuid); - - this.setupDeps = setupDeps; + await this.setupLegacyLogging(setupDeps.http.server); } - public async start(startDeps: LegacyServiceStartDeps) { - const { setupDeps } = this; - - if (!setupDeps || !this.legacyRawConfig) { - throw new Error('Legacy service is not setup yet.'); - } + private async setupLegacyLogging(server: Server) { + const legacyLoggingConfig = await this.legacyLoggingConfig$.pipe(first()).toPromise(); + const currentOpsConfig = await this.opsConfig$.pipe(first()).toPromise(); - this.log.debug('starting legacy service'); + await setupLogging(server, legacyLoggingConfig, currentOpsConfig.interval.asMilliseconds()); + await setupLoggingRotate(server, legacyLoggingConfig); - this.kbnServer = await this.createKbnServer( - this.settings!, - this.legacyRawConfig!, - setupDeps, - startDeps + this.configSubscription = combineLatest([this.legacyLoggingConfig$, this.opsConfig$]).subscribe( + ([newLoggingConfig, newOpsConfig]) => { + reconfigureLogging(server, newLoggingConfig, newOpsConfig.interval.asMilliseconds()); + } ); } @@ -151,156 +71,5 @@ export class LegacyService implements CoreService { this.configSubscription.unsubscribe(); this.configSubscription = undefined; } - - if (this.kbnServer !== undefined) { - await this.kbnServer.close(); - this.kbnServer = undefined; - } - } - - private async createKbnServer( - settings: LegacyVars, - config: LegacyConfig, - setupDeps: LegacyServiceSetupDeps, - startDeps: LegacyServiceStartDeps - ) { - const coreStart: CoreStart = { - capabilities: startDeps.core.capabilities, - elasticsearch: startDeps.core.elasticsearch, - http: { - auth: startDeps.core.http.auth, - basePath: startDeps.core.http.basePath, - getServerInfo: startDeps.core.http.getServerInfo, - }, - savedObjects: { - getScopedClient: startDeps.core.savedObjects.getScopedClient, - createScopedRepository: startDeps.core.savedObjects.createScopedRepository, - createInternalRepository: startDeps.core.savedObjects.createInternalRepository, - createSerializer: startDeps.core.savedObjects.createSerializer, - createExporter: startDeps.core.savedObjects.createExporter, - createImporter: startDeps.core.savedObjects.createImporter, - getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry, - }, - metrics: { - collectionInterval: startDeps.core.metrics.collectionInterval, - getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$, - }, - uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, - coreUsageData: { - getCoreUsageData: () => { - throw new Error('core.start.coreUsageData.getCoreUsageData is unsupported in legacy'); - }, - }, - }; - - const router = setupDeps.core.http.createRouter('', this.legacyId); - const coreSetup: CoreSetup = { - capabilities: setupDeps.core.capabilities, - context: setupDeps.core.context, - elasticsearch: { - legacy: setupDeps.core.elasticsearch.legacy, - }, - http: { - createCookieSessionStorageFactory: setupDeps.core.http.createCookieSessionStorageFactory, - registerRouteHandlerContext: < - Context extends RequestHandlerContext, - ContextName extends keyof Context - >( - contextName: ContextName, - provider: RequestHandlerContextProvider - ) => setupDeps.core.http.registerRouteHandlerContext(this.legacyId, contextName, provider), - createRouter: () => - router as IRouter, - resources: setupDeps.core.httpResources.createRegistrar(router), - registerOnPreRouting: setupDeps.core.http.registerOnPreRouting, - registerOnPreAuth: setupDeps.core.http.registerOnPreAuth, - registerAuth: setupDeps.core.http.registerAuth, - registerOnPostAuth: setupDeps.core.http.registerOnPostAuth, - registerOnPreResponse: setupDeps.core.http.registerOnPreResponse, - basePath: setupDeps.core.http.basePath, - auth: { - get: setupDeps.core.http.auth.get, - isAuthenticated: setupDeps.core.http.auth.isAuthenticated, - }, - csp: setupDeps.core.http.csp, - getServerInfo: setupDeps.core.http.getServerInfo, - }, - i18n: setupDeps.core.i18n, - logging: { - configure: (config$) => setupDeps.core.logging.configure([], config$), - }, - metrics: { - collectionInterval: setupDeps.core.metrics.collectionInterval, - getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$, - }, - savedObjects: { - setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, - addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, - registerType: setupDeps.core.savedObjects.registerType, - }, - status: { - isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous, - core$: setupDeps.core.status.core$, - overall$: setupDeps.core.status.overall$, - set: () => { - throw new Error(`core.status.set is unsupported in legacy`); - }, - // @ts-expect-error - get dependencies$() { - throw new Error(`core.status.dependencies$ is unsupported in legacy`); - }, - // @ts-expect-error - get derivedStatus$() { - throw new Error(`core.status.derivedStatus$ is unsupported in legacy`); - }, - }, - uiSettings: { - register: setupDeps.core.uiSettings.register, - }, - deprecations: { - registerDeprecations: () => { - throw new Error('core.setup.deprecations.registerDeprecations is unsupported in legacy'); - }, - }, - getStartServices: () => Promise.resolve([coreStart, startDeps.plugins, {}]), - }; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const KbnServer = require('../../../legacy/server/kbn_server'); - const kbnServer: LegacyKbnServer = new KbnServer(settings, config, { - env: { - mode: this.coreContext.env.mode, - packageInfo: this.coreContext.env.packageInfo, - }, - setupDeps: { - core: coreSetup, - plugins: setupDeps.plugins, - }, - startDeps: { - core: coreStart, - plugins: startDeps.plugins, - }, - __internals: { - hapiServer: setupDeps.core.http.server, - uiPlugins: setupDeps.uiPlugins, - rendering: setupDeps.core.rendering, - }, - logger: this.coreContext.logger, - }); - - const { autoListen } = await this.httpConfig$.pipe(first()).toPromise(); - - if (autoListen) { - try { - await kbnServer.listen(); - } catch (err) { - await kbnServer.close(); - throw err; - } - } else { - await kbnServer.ready(); - } - - return kbnServer; } } diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.ts b/src/core/server/legacy/logging/appenders/legacy_appender.ts index a89441a5671b..7e02d00c7b23 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.ts @@ -9,11 +9,10 @@ import { schema } from '@kbn/config-schema'; import { LegacyLoggingServer } from '@kbn/legacy-logging'; import { DisposableAppender, LogRecord } from '@kbn/logging'; -import { LegacyVars } from '../../types'; export interface LegacyAppenderConfig { type: 'legacy-appender'; - legacyLoggingConfig?: any; + legacyLoggingConfig?: Record; } /** @@ -23,7 +22,7 @@ export interface LegacyAppenderConfig { export class LegacyAppender implements DisposableAppender { public static configSchema = schema.object({ type: schema.literal('legacy-appender'), - legacyLoggingConfig: schema.any(), + legacyLoggingConfig: schema.recordOf(schema.string(), schema.any()), }); /** @@ -34,7 +33,7 @@ export class LegacyAppender implements DisposableAppender { private readonly loggingServer: LegacyLoggingServer; - constructor(legacyLoggingConfig: Readonly) { + constructor(legacyLoggingConfig: any) { this.loggingServer = new LegacyLoggingServer(legacyLoggingConfig); } diff --git a/src/core/server/legacy/merge_vars.test.ts b/src/core/server/legacy/merge_vars.test.ts deleted file mode 100644 index e4268a52aa8c..000000000000 --- a/src/core/server/legacy/merge_vars.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { mergeVars } from './merge_vars'; - -describe('mergeVars', () => { - it('merges two objects together', () => { - const first = { - otherName: 'value', - otherCanFoo: true, - otherNested: { - otherAnotherVariable: 'ok', - }, - }; - const second = { - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - }; - - expect(mergeVars(first, second)).toEqual({ - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - otherName: 'value', - otherCanFoo: true, - otherNested: { - otherAnotherVariable: 'ok', - }, - }); - }); - - it('does not mutate the source objects', () => { - const first = { - var1: 'first', - }; - const second = { - var1: 'second', - var2: 'second', - }; - const third = { - var1: 'third', - var2: 'third', - var3: 'third', - }; - const fourth = { - var1: 'fourth', - var2: 'fourth', - var3: 'fourth', - var4: 'fourth', - }; - - mergeVars(first, second, third, fourth); - - expect(first).toEqual({ var1: 'first' }); - expect(second).toEqual({ var1: 'second', var2: 'second' }); - expect(third).toEqual({ var1: 'third', var2: 'third', var3: 'third' }); - expect(fourth).toEqual({ var1: 'fourth', var2: 'fourth', var3: 'fourth', var4: 'fourth' }); - }); - - it('merges multiple objects together with precedence increasing from left-to-right', () => { - const first = { - var1: 'first', - var2: 'first', - var3: 'first', - var4: 'first', - }; - const second = { - var1: 'second', - var2: 'second', - var3: 'second', - }; - const third = { - var1: 'third', - var2: 'third', - }; - const fourth = { - var1: 'fourth', - }; - - expect(mergeVars(first, second, third, fourth)).toEqual({ - var1: 'fourth', - var2: 'third', - var3: 'second', - var4: 'first', - }); - }); - - it('overwrites the original variable value if a duplicate entry is found', () => { - const first = { - nested: { - otherAnotherVariable: 'ok', - }, - }; - const second = { - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - }; - - expect(mergeVars(first, second)).toEqual({ - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - }); - }); - - it('combines entries within "uiCapabilities"', () => { - const first = { - uiCapabilities: { - firstCapability: 'ok', - sharedCapability: 'shared', - }, - }; - const second = { - name: 'value', - canFoo: true, - uiCapabilities: { - secondCapability: 'ok', - }, - }; - const third = { - name: 'value', - canFoo: true, - uiCapabilities: { - thirdCapability: 'ok', - sharedCapability: 'blocked', - }, - }; - - expect(mergeVars(first, second, third)).toEqual({ - name: 'value', - canFoo: true, - uiCapabilities: { - firstCapability: 'ok', - secondCapability: 'ok', - thirdCapability: 'ok', - sharedCapability: 'blocked', - }, - }); - }); - - it('does not deeply combine entries within "uiCapabilities"', () => { - const first = { - uiCapabilities: { - firstCapability: 'ok', - nestedCapability: { - otherNestedProp: 'otherNestedValue', - }, - }, - }; - const second = { - name: 'value', - canFoo: true, - uiCapabilities: { - secondCapability: 'ok', - nestedCapability: { - nestedProp: 'nestedValue', - }, - }, - }; - - expect(mergeVars(first, second)).toEqual({ - name: 'value', - canFoo: true, - uiCapabilities: { - firstCapability: 'ok', - secondCapability: 'ok', - nestedCapability: { - nestedProp: 'nestedValue', - }, - }, - }); - }); -}); diff --git a/src/core/server/legacy/merge_vars.ts b/src/core/server/legacy/merge_vars.ts deleted file mode 100644 index cd2cbb0d8cde..000000000000 --- a/src/core/server/legacy/merge_vars.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LegacyVars } from './types'; - -const ELIGIBLE_FLAT_MERGE_KEYS = ['uiCapabilities']; - -export function mergeVars(...sources: LegacyVars[]): LegacyVars { - return Object.assign( - {}, - ...sources, - ...ELIGIBLE_FLAT_MERGE_KEYS.flatMap((key) => - sources.some((source) => key in source) - ? [{ [key]: Object.assign({}, ...sources.map((source) => source[key] || {})) }] - : [] - ) - ); -} diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts deleted file mode 100644 index 9f562d3da302..000000000000 --- a/src/core/server/legacy/types.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; -import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins'; -import { InternalRenderingServiceSetup } from '../rendering'; - -/** - * @internal - * @deprecated - */ -export type LegacyVars = Record; - -type LegacyCoreSetup = InternalCoreSetup & { - plugins: PluginsServiceSetup; - rendering: InternalRenderingServiceSetup; -}; -type LegacyCoreStart = InternalCoreStart & { plugins: PluginsServiceStart }; - -/** - * New platform representation of the legacy configuration (KibanaConfig) - * - * @internal - * @deprecated - */ -export interface LegacyConfig { - get(key?: string): T; - has(key: string): boolean; - set(key: string, value: any): void; - set(config: LegacyVars): void; -} - -/** - * @public - * @deprecated - */ -export interface LegacyServiceSetupDeps { - core: LegacyCoreSetup; - plugins: Record; - uiPlugins: UiPlugins; -} - -/** - * @public - * @deprecated - */ -export interface LegacyServiceStartDeps { - core: LegacyCoreStart; - plugins: Record; -} - -/** - * @internal - * @deprecated - */ -export interface LegacyServiceSetupConfig { - legacyConfig: LegacyConfig; - settings: LegacyVars; -} diff --git a/src/core/server/logging/__snapshots__/logging_config.test.ts.snap b/src/core/server/logging/__snapshots__/logging_config.test.ts.snap deleted file mode 100644 index fe1407563a63..000000000000 --- a/src/core/server/logging/__snapshots__/logging_config.test.ts.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`\`schema\` creates correct schema with defaults. 1`] = ` -Object { - "appenders": Map {}, - "loggers": Array [], - "root": Object { - "appenders": Array [ - "default", - ], - "level": "info", - }, -} -`; - -exports[`\`schema\` throws if \`root\` logger does not have "default" appender configured. 1`] = `"[root]: \\"default\\" appender required for migration period till the next major release"`; - -exports[`\`schema\` throws if \`root\` logger does not have appenders configured. 1`] = `"[root.appenders]: array size is [0], but cannot be smaller than [1]"`; - -exports[`fails if loggers use unknown appenders. 1`] = `"Logger \\"some.nested.context\\" contains unsupported appender key \\"unknown\\"."`; diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index 83f3c139e371..e0004ba992c1 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -9,7 +9,35 @@ import { LoggingConfig, config } from './logging_config'; test('`schema` creates correct schema with defaults.', () => { - expect(config.schema.validate({})).toMatchSnapshot(); + expect(config.schema.validate({})).toMatchInlineSnapshot( + { json: expect.any(Boolean) }, // default value depends on TTY + ` + Object { + "appenders": Map {}, + "dest": "stdout", + "events": Object {}, + "filter": Object {}, + "json": Any, + "loggers": Array [], + "quiet": false, + "root": Object { + "appenders": Array [ + "default", + ], + "level": "info", + }, + "rotate": Object { + "enabled": false, + "everyBytes": 10485760, + "keepFiles": 7, + "pollingInterval": 10000, + "usePolling": false, + }, + "silent": false, + "verbose": false, + } + ` + ); }); test('`schema` throws if `root` logger does not have appenders configured.', () => { @@ -19,7 +47,9 @@ test('`schema` throws if `root` logger does not have appenders configured.', () appenders: [], }, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[root.appenders]: array size is [0], but cannot be smaller than [1]"` + ); }); test('`schema` throws if `root` logger does not have "default" appender configured.', () => { @@ -29,7 +59,9 @@ test('`schema` throws if `root` logger does not have "default" appender configur appenders: ['console'], }, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[root]: \\"default\\" appender required for migration period till the next major release"` + ); }); test('`getParentLoggerContext()` returns correct parent context name.', () => { @@ -157,7 +189,9 @@ test('fails if loggers use unknown appenders.', () => { ], }); - expect(() => new LoggingConfig(validateConfig)).toThrowErrorMatchingSnapshot(); + expect(() => new LoggingConfig(validateConfig)).toThrowErrorMatchingInlineSnapshot( + `"Logger \\"some.nested.context\\" contains unsupported appender key \\"unknown\\"."` + ); }); describe('extend', () => { diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index 24496289fb4c..f5b75d7bb739 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -7,6 +7,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { legacyLoggingConfigSchema } from '@kbn/legacy-logging'; import { AppenderConfigType, Appenders } from './appenders/appenders'; // We need this helper for the types to be correct @@ -59,7 +60,7 @@ export const loggerSchema = schema.object({ export type LoggerConfigType = TypeOf; export const config = { path: 'logging', - schema: schema.object({ + schema: legacyLoggingConfigSchema.extends({ appenders: schema.mapOf(schema.string(), Appenders.configSchema, { defaultValue: new Map(), }), @@ -85,7 +86,7 @@ export const config = { }), }; -export type LoggingConfigType = Omit, 'appenders'> & { +export type LoggingConfigType = Pick, 'loggers' | 'root'> & { appenders: Map; }; @@ -105,6 +106,7 @@ export const loggerContextConfigSchema = schema.object({ /** @public */ export type LoggerContextConfigType = TypeOf; + /** @public */ export interface LoggerContextConfigInput { // config-schema knows how to handle either Maps or Records diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts index 8a6fe71bc622..b67be384732c 100644 --- a/src/core/server/logging/logging_system.test.ts +++ b/src/core/server/logging/logging_system.test.ts @@ -16,6 +16,7 @@ jest.mock('fs', () => ({ const dynamicProps = { process: { pid: expect.any(Number) } }; jest.mock('@kbn/legacy-logging', () => ({ + ...(jest.requireActual('@kbn/legacy-logging') as any), setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})), })); diff --git a/src/core/server/metrics/index.ts b/src/core/server/metrics/index.ts index 3e358edf3a01..0631bb2b3580 100644 --- a/src/core/server/metrics/index.ts +++ b/src/core/server/metrics/index.ts @@ -16,3 +16,4 @@ export type { export type { OpsProcessMetrics, OpsServerMetrics, OpsOsMetrics } from './collectors'; export { MetricsService } from './metrics_service'; export { opsConfig } from './ops_config'; +export type { OpsConfigType } from './ops_config'; diff --git a/src/core/server/plugins/legacy_config.test.ts b/src/core/server/plugins/legacy_config.test.ts index 5687c2dd551d..0ea26f2e0333 100644 --- a/src/core/server/plugins/legacy_config.test.ts +++ b/src/core/server/plugins/legacy_config.test.ts @@ -13,7 +13,7 @@ import { getGlobalConfig, getGlobalConfig$ } from './legacy_config'; import { REPO_ROOT } from '@kbn/utils'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { duration } from 'moment'; -import { fromRoot } from '../utils'; +import { fromRoot } from '@kbn/utils'; import { ByteSizeValue } from '@kbn/config-schema'; import { Server } from '../server'; diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index b10bc47cb825..e37d985d4232 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -9,6 +9,7 @@ import { duration } from 'moment'; import { first } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/dev-utils'; +import { fromRoot } from '@kbn/utils'; import { createPluginInitializerContext, InstanceInfo } from './plugin_context'; import { CoreContext } from '../core_context'; import { Env } from '../config'; @@ -16,7 +17,6 @@ import { loggingSystemMock } from '../logging/logging_system.mock'; import { rawConfigServiceMock, getEnvOptions } from '../config/mocks'; import { PluginManifest } from './types'; import { Server } from '../server'; -import { fromRoot } from '../utils'; import { schema, ByteSizeValue } from '@kbn/config-schema'; import { ConfigService } from '@kbn/config'; diff --git a/src/core/server/plugins/plugins_config.ts b/src/core/server/plugins/plugins_config.ts index d565513ebb35..45d80445f376 100644 --- a/src/core/server/plugins/plugins_config.ts +++ b/src/core/server/plugins/plugins_config.ts @@ -7,20 +7,24 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { ServiceConfigDescriptor } from '../internal_types'; import { Env } from '../config'; -export type PluginsConfigType = TypeOf; +const configSchema = schema.object({ + initialize: schema.boolean({ defaultValue: true }), -export const config = { + /** + * Defines an array of directories where another plugin should be loaded from. + */ + paths: schema.arrayOf(schema.string(), { defaultValue: [] }), +}); + +export type PluginsConfigType = TypeOf; + +export const config: ServiceConfigDescriptor = { path: 'plugins', - schema: schema.object({ - initialize: schema.boolean({ defaultValue: true }), - - /** - * Defines an array of directories where another plugin should be loaded from. - */ - paths: schema.arrayOf(schema.string(), { defaultValue: [] }), - }), + schema: configSchema, + deprecations: ({ unusedFromRoot }) => [unusedFromRoot('plugins.scanDirs')], }; /** @internal */ diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 2d54648d2295..6bf7a1fadb4d 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -562,12 +562,12 @@ describe('PluginsService', () => { plugin$: from([ createPlugin('plugin-1', { path: 'path-1', - version: 'some-version', + version: 'version-1', configPath: 'plugin1', }), createPlugin('plugin-2', { path: 'path-2', - version: 'some-version', + version: 'version-2', configPath: 'plugin2', }), ]), @@ -577,7 +577,7 @@ describe('PluginsService', () => { }); describe('uiPlugins.internal', () => { - it('includes disabled plugins', async () => { + it('contains internal properties for plugins', async () => { config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); expect(uiPlugins.internal).toMatchInlineSnapshot(` @@ -586,15 +586,23 @@ describe('PluginsService', () => { "publicAssetsDir": /path-1/public/assets, "publicTargetDir": /path-1/target/public, "requiredBundles": Array [], + "version": "version-1", }, "plugin-2" => Object { "publicAssetsDir": /path-2/public/assets, "publicTargetDir": /path-2/target/public, "requiredBundles": Array [], + "version": "version-2", }, } `); }); + + it('includes disabled plugins', async () => { + config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); + const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); + expect([...uiPlugins.internal.keys()].sort()).toEqual(['plugin-1', 'plugin-2']); + }); }); describe('plugin initialization', () => { diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 8b33e2cf4cc6..09be40ecaf2a 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -222,6 +222,7 @@ export class PluginsService implements CoreService(); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index a6086bd6f17e..3a01049c5e1f 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -224,12 +224,15 @@ export interface DiscoveredPlugin { */ export interface InternalPluginInfo { /** - * Bundles that must be loaded for this plugoin + * Version of the plugin + */ + readonly version: string; + /** + * Bundles that must be loaded for this plugin */ readonly requiredBundles: readonly string[]; /** - * Path to the target/public directory of the plugin which should be - * served + * Path to the target/public directory of the plugin which should be served */ readonly publicTargetDir: string; /** @@ -250,7 +253,9 @@ export interface Plugin< TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; } @@ -267,7 +272,9 @@ export interface AsyncPlugin< TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + stop?(): void; } diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts index ea3843884df3..0abd8fd5a005 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { UiPlugins } from '../../plugins'; +import { InternalPluginInfo, UiPlugins } from '../../plugins'; import { getPluginsBundlePaths } from './get_plugin_bundle_paths'; const createUiPlugins = (pluginDeps: Record) => { @@ -16,12 +16,13 @@ const createUiPlugins = (pluginDeps: Record) => { browserConfigs: new Map(), }; - Object.entries(pluginDeps).forEach(([pluginId, deps]) => { + const addPlugin = (pluginId: string, deps: string[]) => { uiPlugins.internal.set(pluginId, { requiredBundles: deps, + version: '8.0.0', publicTargetDir: '', publicAssetsDir: '', - } as any); + } as InternalPluginInfo); uiPlugins.public.set(pluginId, { id: pluginId, configPath: 'config-path', @@ -29,6 +30,12 @@ const createUiPlugins = (pluginDeps: Record) => { requiredPlugins: [], requiredBundles: deps, }); + + deps.forEach((dep) => addPlugin(dep, [])); + }; + + Object.entries(pluginDeps).forEach(([pluginId, deps]) => { + addPlugin(pluginId, deps); }); return uiPlugins; @@ -56,13 +63,13 @@ describe('getPluginsBundlePaths', () => { }); expect(pluginBundlePaths.get('a')).toEqual({ - bundlePath: '/regular-bundle-path/plugin/a/a.plugin.js', - publicPath: '/regular-bundle-path/plugin/a/', + bundlePath: '/regular-bundle-path/plugin/a/8.0.0/a.plugin.js', + publicPath: '/regular-bundle-path/plugin/a/8.0.0/', }); expect(pluginBundlePaths.get('b')).toEqual({ - bundlePath: '/regular-bundle-path/plugin/b/b.plugin.js', - publicPath: '/regular-bundle-path/plugin/b/', + bundlePath: '/regular-bundle-path/plugin/b/8.0.0/b.plugin.js', + publicPath: '/regular-bundle-path/plugin/b/8.0.0/', }); }); }); diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts index c8291b2720a9..86ffdcf835f7 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts @@ -25,9 +25,15 @@ export const getPluginsBundlePaths = ({ while (pluginsToProcess.length > 0) { const pluginId = pluginsToProcess.pop() as string; + const plugin = uiPlugins.internal.get(pluginId); + if (!plugin) { + continue; + } + const { version } = plugin; + pluginBundlePaths.set(pluginId, { - publicPath: `${regularBundlePath}/plugin/${pluginId}/`, - bundlePath: `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js`, + publicPath: `${regularBundlePath}/plugin/${pluginId}/${version}/`, + bundlePath: `${regularBundlePath}/plugin/${pluginId}/${version}/${pluginId}.plugin.js`, }); const pluginBundleIds = uiPlugins.internal.get(pluginId)?.requiredBundles ?? []; diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts index a21078cbe113..14ca73e7fcca 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts @@ -163,7 +163,12 @@ describe('actions', () => { describe('searchForOutdatedDocuments', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.searchForOutdatedDocuments(client, 'new_index', { properties: {} }); + const task = Actions.searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'new_index', + outdatedDocumentsQuery: {}, + }); + try { await task(); } catch (e) { @@ -172,6 +177,29 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); + + it('configures request according to given parameters', async () => { + const esClient = elasticsearchClientMock.createInternalClient(); + const query = {}; + const targetIndex = 'new_index'; + const batchSize = 1000; + const task = Actions.searchForOutdatedDocuments(esClient, { + batchSize, + targetIndex, + outdatedDocumentsQuery: query, + }); + + await task(); + + expect(esClient.search).toHaveBeenCalledTimes(1); + expect(esClient.search).toHaveBeenCalledWith( + expect.objectContaining({ + index: targetIndex, + size: batchSize, + body: expect.objectContaining({ query }), + }) + ); + }); }); describe('bulkOverwriteTransformedDocuments', () => { diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 52fa99b72487..8ac683a29d65 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -9,11 +9,11 @@ import * as Either from 'fp-ts/lib/Either'; import * as TaskEither from 'fp-ts/lib/TaskEither'; import * as Option from 'fp-ts/lib/Option'; -import { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors'; -import { pipe } from 'fp-ts/lib/pipeable'; +import type { estypes } from '@elastic/elasticsearch'; import { errors as EsErrors } from '@elastic/elasticsearch'; +import type { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { pipe } from 'fp-ts/lib/pipeable'; import { flow } from 'fp-ts/lib/function'; -import type { estypes } from '@elastic/elasticsearch'; import { ElasticsearchClient } from '../../../elasticsearch'; import { IndexMapping } from '../../mappings'; import { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization'; @@ -24,13 +24,10 @@ import { export type { RetryableEsClientError }; /** - * Batch size for updateByQuery, reindex & search operations. Smaller batches - * reduce the memory pressure on Elasticsearch and Kibana so are less likely - * to cause failures. - * TODO (profile/tune): How much smaller can we make this number before it - * starts impacting how long migrations take to perform? + * Batch size for updateByQuery and reindex operations. + * Uses the default value of 1000 for Elasticsearch reindex operation. */ -const BATCH_SIZE = 1000; +const BATCH_SIZE = 1_000; const DEFAULT_TIMEOUT = '60s'; /** Allocate 1 replica if there are enough data nodes, otherwise continue with 0 */ const INDEX_AUTO_EXPAND_REPLICAS = '0-1'; @@ -839,6 +836,12 @@ export interface SearchResponse { outdatedDocuments: SavedObjectsRawDoc[]; } +interface SearchForOutdatedDocumentsOptions { + batchSize: number; + targetIndex: string; + outdatedDocumentsQuery?: estypes.QueryContainer; +} + /** * Search for outdated saved object documents with the provided query. Will * return one batch of documents. Searching should be repeated until no more @@ -846,18 +849,17 @@ export interface SearchResponse { */ export const searchForOutdatedDocuments = ( client: ElasticsearchClient, - index: string, - query: Record + options: SearchForOutdatedDocumentsOptions ): TaskEither.TaskEither => () => { return client .search({ - index, + index: options.targetIndex, // Return the _seq_no and _primary_term so we can use optimistic // concurrency control for updates seq_no_primary_term: true, - size: BATCH_SIZE, + size: options.batchSize, body: { - query, + query: options.outdatedDocumentsQuery, // Optimize search performance by sorting by the "natural" index order sort: ['_doc'], }, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 1824efa0ed8d..aa9a5ea92ac1 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -59,7 +59,7 @@ describe('migration actions', () => { // Create test fixture data: await createIndex(client, 'existing_index_with_docs', { - dynamic: true as any, + dynamic: true, properties: {}, })(); const sourceDocs = ([ @@ -337,7 +337,6 @@ describe('migration actions', () => { // Reindex doesn't return any errors on it's own, so we have to test // together with waitForReindexTask describe('reindex & waitForReindexTask', () => { - expect.assertions(2); it('resolves right when reindex succeeds without reindex script', async () => { const res = (await reindex( client, @@ -354,11 +353,11 @@ describe('migration actions', () => { } `); - const results = ((await searchForOutdatedDocuments( - client, - 'reindex_target', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'reindex_target', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` Array [ "doc 1", @@ -384,11 +383,11 @@ describe('migration actions', () => { "right": "reindex_succeeded", } `); - const results = ((await searchForOutdatedDocuments( - client, - 'reindex_target_2', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'reindex_target_2', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` Array [ "doc 1_updated", @@ -432,12 +431,12 @@ describe('migration actions', () => { } `); - // Assert that documents weren't overrided by the second, unscripted reindex - const results = ((await searchForOutdatedDocuments( - client, - 'reindex_target_3', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + // Assert that documents weren't overridden by the second, unscripted reindex + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'reindex_target_3', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` Array [ "doc 1_updated", @@ -452,11 +451,11 @@ describe('migration actions', () => { // Simulate a reindex that only adds some of the documents from the // source index into the target index await createIndex(client, 'reindex_target_4', { properties: {} })(); - const sourceDocs = ((await searchForOutdatedDocuments( - client, - 'existing_index_with_docs', - undefined as any - )()) as Either.Right).right.outdatedDocuments + const sourceDocs = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_with_docs', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments .slice(0, 2) .map(({ _id, _source }) => ({ _id, @@ -479,13 +478,13 @@ describe('migration actions', () => { "right": "reindex_succeeded", } `); - // Assert that existing documents weren't overrided, but that missing + // Assert that existing documents weren't overridden, but that missing // documents were added by the reindex - const results = ((await searchForOutdatedDocuments( - client, - 'reindex_target_4', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'reindex_target_4', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` Array [ "doc 1", @@ -701,26 +700,30 @@ describe('migration actions', () => { describe('searchForOutdatedDocuments', () => { it('only returns documents that match the outdatedDocumentsQuery', async () => { expect.assertions(2); - const resultsWithQuery = ((await searchForOutdatedDocuments( - client, - 'existing_index_with_docs', - { + const resultsWithQuery = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_with_docs', + outdatedDocumentsQuery: { match: { title: { query: 'doc' } }, - } - )()) as Either.Right).right.outdatedDocuments; + }, + })()) as Either.Right).right.outdatedDocuments; expect(resultsWithQuery.length).toBe(3); - const resultsWithoutQuery = ((await searchForOutdatedDocuments( - client, - 'existing_index_with_docs', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + const resultsWithoutQuery = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_with_docs', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; expect(resultsWithoutQuery.length).toBe(4); }); it('resolves with _id, _source, _seq_no and _primary_term', async () => { expect.assertions(1); - const results = ((await searchForOutdatedDocuments(client, 'existing_index_with_docs', { - match: { title: { query: 'doc' } }, + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_with_docs', + outdatedDocumentsQuery: { + match: { title: { query: 'doc' } }, + }, })()) as Either.Right).right.outdatedDocuments; expect(results).toEqual( expect.arrayContaining([ @@ -805,7 +808,7 @@ describe('migration actions', () => { it('resolves right when mappings were updated and picked up', async () => { // Create an index without any mappings and insert documents into it await createIndex(client, 'existing_index_without_mappings', { - dynamic: false as any, + dynamic: false, properties: {}, })(); const sourceDocs = ([ @@ -821,11 +824,13 @@ describe('migration actions', () => { )(); // Assert that we can't search over the unmapped fields of the document - const originalSearchResults = ((await searchForOutdatedDocuments( - client, - 'existing_index_without_mappings', - { match: { title: { query: 'doc' } } } - )()) as Either.Right).right.outdatedDocuments; + const originalSearchResults = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_without_mappings', + outdatedDocumentsQuery: { + match: { title: { query: 'doc' } }, + }, + })()) as Either.Right).right.outdatedDocuments; expect(originalSearchResults.length).toBe(0); // Update and pickup mappings so that the title field is searchable @@ -839,11 +844,13 @@ describe('migration actions', () => { await waitForPickupUpdatedMappingsTask(client, taskId, '60s')(); // Repeat the search expecting to be able to find the existing documents - const pickedUpSearchResults = ((await searchForOutdatedDocuments( - client, - 'existing_index_without_mappings', - { match: { title: { query: 'doc' } } } - )()) as Either.Right).right.outdatedDocuments; + const pickedUpSearchResults = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_without_mappings', + outdatedDocumentsQuery: { + match: { title: { query: 'doc' } }, + }, + })()) as Either.Right).right.outdatedDocuments; expect(pickedUpSearchResults.length).toBe(4); }); }); @@ -1050,11 +1057,11 @@ describe('migration actions', () => { `); }); it('resolves right even if there were some version_conflict_engine_exception', async () => { - const existingDocs = ((await searchForOutdatedDocuments( - client, - 'existing_index_with_docs', - undefined as any - )()) as Either.Right).right.outdatedDocuments; + const existingDocs = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'existing_index_with_docs', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; const task = bulkOverwriteTransformedDocuments(client, 'existing_index_with_docs', [ ...existingDocs, diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts index 99c06c0a3586..d4ce7b74baa5 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts @@ -206,6 +206,7 @@ describe('migrationsStateActionMachine', () => { Array [ "[.my-so-index] INIT -> LEGACY_DELETE", Object { + "batchSize": 1000, "controlState": "LEGACY_DELETE", "currentAlias": ".my-so-index", "indexPrefix": ".my-so-index", @@ -262,6 +263,7 @@ describe('migrationsStateActionMachine', () => { Array [ "[.my-so-index] LEGACY_DELETE -> FATAL", Object { + "batchSize": 1000, "controlState": "FATAL", "currentAlias": ".my-so-index", "indexPrefix": ".my-so-index", @@ -413,6 +415,7 @@ describe('migrationsStateActionMachine', () => { Array [ "[.my-so-index] INIT -> LEGACY_REINDEX", Object { + "batchSize": 1000, "controlState": "LEGACY_REINDEX", "currentAlias": ".my-so-index", "indexPrefix": ".my-so-index", @@ -464,6 +467,7 @@ describe('migrationsStateActionMachine', () => { Array [ "[.my-so-index] LEGACY_REINDEX -> LEGACY_DELETE", Object { + "batchSize": 1000, "controlState": "LEGACY_DELETE", "currentAlias": ".my-so-index", "indexPrefix": ".my-so-index", diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 2813f01093e9..f9bf3418c0ab 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -46,6 +46,7 @@ describe('migrations v2 model', () => { retryCount: 0, retryDelay: 0, retryAttempts: 15, + batchSize: 1000, indexPrefix: '.kibana', outdatedDocumentsQuery: {}, targetIndexMappings: { @@ -1182,6 +1183,7 @@ describe('migrations v2 model', () => { describe('createInitialState', () => { const migrationsConfig = ({ retryAttempts: 15, + batchSize: 1000, } as unknown) as SavedObjectsMigrationConfigType; it('creates the initial state for the model based on the passed in paramaters', () => { expect( @@ -1197,6 +1199,7 @@ describe('migrations v2 model', () => { }) ).toMatchInlineSnapshot(` Object { + "batchSize": 1000, "controlState": "INIT", "currentAlias": ".kibana_task_manager", "indexPrefix": ".kibana_task_manager", diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 5bdba9802679..e62bd108faea 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -784,6 +784,7 @@ export const createInitialState = ({ retryCount: 0, retryDelay: 0, retryAttempts: migrationsConfig.retryAttempts, + batchSize: migrationsConfig.batchSize, logs: [], }; return initialState; diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 1b594cf3d8b5..5c159f4f24e2 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -73,7 +73,11 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK: (state: UpdateTargetMappingsWaitForTaskState) => Actions.waitForPickupUpdatedMappingsTask(client, state.updateTargetMappingsTaskId, '60s'), OUTDATED_DOCUMENTS_SEARCH: (state: OutdatedDocumentsSearch) => - Actions.searchForOutdatedDocuments(client, state.targetIndex, state.outdatedDocumentsQuery), + Actions.searchForOutdatedDocuments(client, { + batchSize: state.batchSize, + targetIndex: state.targetIndex, + outdatedDocumentsQuery: state.outdatedDocumentsQuery, + }), OUTDATED_DOCUMENTS_TRANSFORM: (state: OutdatedDocumentsTransform) => pipe( TaskEither.tryCatch( diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index dbdd5774dfa6..8d6fe3f030eb 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -54,6 +54,21 @@ export interface BaseState extends ControlState { * max_retry_time = 11.7 minutes */ readonly retryAttempts: number; + + /** + * The number of documents to fetch from Elasticsearch server to run migration over. + * + * The higher the value, the faster the migration process will be performed since it reduces + * the number of round trips between Kibana and Elasticsearch servers. + * For the migration speed, we have to pay the price of increased memory consumption. + * + * Since batchSize defines the number of documents, not their size, it might happen that + * Elasticsearch fails a request with circuit_breaking_exception when it retrieves a set of + * saved objects of significant size. + * + * In this case, you should set a smaller batchSize value and restart the migration process again. + */ + readonly batchSize: number; readonly logs: Array<{ level: 'error' | 'info'; message: string }>; /** * The current alias e.g. `.kibana` which always points to the latest diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 7228cb126d28..96fac85ded07 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -29,8 +29,8 @@ export type SavedObjectsConfigType = TypeOf; export const savedObjectsConfig = { path: 'savedObjects', schema: schema.object({ - maxImportPayloadBytes: schema.byteSize({ defaultValue: 26214400 }), - maxImportExportSize: schema.number({ defaultValue: 10000 }), + maxImportPayloadBytes: schema.byteSize({ defaultValue: 26_214_400 }), + maxImportExportSize: schema.number({ defaultValue: 10_000 }), }), }; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index de96c5ccfb81..53b2eb861041 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -142,7 +142,6 @@ import { SearchParams } from 'elasticsearch'; import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; import { SearchShardsParams } from 'elasticsearch'; import { SearchTemplateParams } from 'elasticsearch'; -import { Server } from '@hapi/hapi'; import { ShallowPromise } from '@kbn/utility-types'; import { SnapshotCreateParams } from 'elasticsearch'; import { SnapshotCreateRepositoryParams } from 'elasticsearch'; @@ -345,7 +344,7 @@ export const config: { pingTimeout: Type; logQueries: Type; ssl: import("@kbn/config-schema").ObjectType<{ - verificationMode: Type<"certificate" | "none" | "full">; + verificationMode: Type<"none" | "certificate" | "full">; certificateAuthorities: Type; certificate: Type; key: Type; @@ -1305,10 +1304,10 @@ export type KibanaResponseFactory = typeof kibanaResponseFactory; // @public export const kibanaResponseFactory: { - custom: | Error | Buffer | { + custom: | Error | Buffer | Stream | { message: string | Error; attributes?: Record | undefined; - } | Stream | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; + } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; badRequest: (options?: ErrorHttpResponseOptions) => KibanaResponse; unauthorized: (options?: ErrorHttpResponseOptions) => KibanaResponse; forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; @@ -1585,20 +1584,6 @@ export class LegacyClusterClient implements ILegacyClusterClient { close(): void; } -// @internal @deprecated -export interface LegacyConfig { - // (undocumented) - get(key?: string): T; - // (undocumented) - has(key: string): boolean; - // (undocumented) - set(key: string, value: any): void; - // Warning: (ae-forgotten-export) The symbol "LegacyVars" needs to be exported by the entry point index.d.ts - // - // (undocumented) - set(config: LegacyVars): void; -} - // @public @deprecated (undocumented) export type LegacyElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; @@ -1634,30 +1619,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { callAsInternalUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } -// @public @deprecated (undocumented) -export interface LegacyServiceSetupDeps { - // Warning: (ae-forgotten-export) The symbol "LegacyCoreSetup" needs to be exported by the entry point index.d.ts - // - // (undocumented) - core: LegacyCoreSetup; - // (undocumented) - plugins: Record; - // Warning: (ae-forgotten-export) The symbol "UiPlugins" needs to be exported by the entry point index.d.ts - // - // (undocumented) - uiPlugins: UiPlugins; -} - -// @public @deprecated (undocumented) -export interface LegacyServiceStartDeps { - // Warning: (ae-forgotten-export) The symbol "LegacyCoreStart" needs to be exported by the entry point index.d.ts - // - // (undocumented) - core: LegacyCoreStart; - // (undocumented) - plugins: Record; -} - // Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts // // @public @@ -3259,9 +3220,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/elasticsearch/client/types.ts:94:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts // src/core/server/http/router/response.ts:297:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:289:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:394:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:293:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:293:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:296:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:401:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 96047dc6921e..2bd3028b2f1b 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -58,7 +58,7 @@ jest.doMock('./ui_settings/ui_settings_service', () => ({ })); export const mockEnsureValidConfiguration = jest.fn(); -jest.doMock('./legacy/config/ensure_valid_configuration', () => ({ +jest.doMock('./config/ensure_valid_configuration', () => ({ ensureValidConfiguration: mockEnsureValidConfiguration, })); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index fcf09b0295bc..534d7df9d946 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -99,7 +99,6 @@ test('injects legacy dependency to context#setup()', async () => { pluginDependencies: new Map([ [pluginA, []], [pluginB, [pluginA]], - [mockLegacyService.legacyId, [pluginA, pluginB]], ]), }); }); @@ -108,12 +107,10 @@ test('runs services on "start"', async () => { const server = new Server(rawConfigService, env, logger); expect(mockHttpService.setup).not.toHaveBeenCalled(); - expect(mockLegacyService.start).not.toHaveBeenCalled(); await server.setup(); expect(mockHttpService.start).not.toHaveBeenCalled(); - expect(mockLegacyService.start).not.toHaveBeenCalled(); expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); expect(mockUiSettingsService.start).not.toHaveBeenCalled(); expect(mockMetricsService.start).not.toHaveBeenCalled(); @@ -121,7 +118,6 @@ test('runs services on "start"', async () => { await server.start(); expect(mockHttpService.start).toHaveBeenCalledTimes(1); - expect(mockLegacyService.start).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); expect(mockMetricsService.start).toHaveBeenCalledTimes(1); @@ -164,26 +160,6 @@ test('stops services on "stop"', async () => { }); test(`doesn't setup core services if config validation fails`, async () => { - mockConfigService.validate.mockImplementationOnce(() => { - return Promise.reject(new Error('invalid config')); - }); - const server = new Server(rawConfigService, env, logger); - await expect(server.setup()).rejects.toThrowErrorMatchingInlineSnapshot(`"invalid config"`); - - expect(mockHttpService.setup).not.toHaveBeenCalled(); - expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); - expect(mockPluginsService.setup).not.toHaveBeenCalled(); - expect(mockLegacyService.setup).not.toHaveBeenCalled(); - expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); - expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); - expect(mockRenderingService.setup).not.toHaveBeenCalled(); - expect(mockMetricsService.setup).not.toHaveBeenCalled(); - expect(mockStatusService.setup).not.toHaveBeenCalled(); - expect(mockLoggingService.setup).not.toHaveBeenCalled(); - expect(mockI18nService.setup).not.toHaveBeenCalled(); -}); - -test(`doesn't setup core services if legacy config validation fails`, async () => { mockEnsureValidConfiguration.mockImplementation(() => { throw new Error('Unknown configuration keys'); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index b575b2779082..b34d7fec3dcb 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -8,15 +8,20 @@ import apm from 'elastic-apm-node'; import { config as pathConfig } from '@kbn/utils'; -import { mapToObject } from '@kbn/std'; -import { ConfigService, Env, RawConfigurationProvider, coreDeprecationProvider } from './config'; +import { + ConfigService, + Env, + RawConfigurationProvider, + coreDeprecationProvider, + ensureValidConfiguration, +} from './config'; import { CoreApp } from './core_app'; import { I18nService } from './i18n'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; import { HttpResourcesService } from './http_resources'; import { RenderingService } from './rendering'; -import { LegacyService, ensureValidConfiguration } from './legacy'; +import { LegacyService } from './legacy'; import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; @@ -121,22 +126,13 @@ export class Server { const { pluginTree, pluginPaths, uiPlugins } = await this.plugins.discover({ environment: environmentSetup, }); - const legacyConfigSetup = await this.legacy.setupLegacyConfig(); // Immediately terminate in case of invalid configuration // This needs to be done after plugin discovery - await this.configService.validate(); - await ensureValidConfiguration(this.configService, legacyConfigSetup); + await ensureValidConfiguration(this.configService); const contextServiceSetup = this.context.setup({ - // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: - // 1) Can access context from any KP plugin - // 2) Can register context providers that will only be available to other legacy plugins and will not leak into - // New Platform plugins. - pluginDependencies: new Map([ - ...pluginTree.asOpaqueIds, - [this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]], - ]), + pluginDependencies: new Map([...pluginTree.asOpaqueIds]), }); const httpSetup = await this.http.setup({ @@ -222,9 +218,7 @@ export class Server { this.#pluginsInitialized = pluginsSetup.initialized; await this.legacy.setup({ - core: { ...coreSetup, plugins: pluginsSetup, rendering: renderingSetup }, - plugins: mapToObject(pluginsSetup.contracts), - uiPlugins, + http: httpSetup, }); this.registerCoreContext(coreSetup); @@ -266,15 +260,7 @@ export class Server { coreUsageData: coreUsageDataStart, }; - const pluginsStart = await this.plugins.start(this.coreStart); - - await this.legacy.start({ - core: { - ...this.coreStart, - plugins: pluginsStart, - }, - plugins: mapToObject(pluginsStart.contracts), - }); + await this.plugins.start(this.coreStart); await this.http.start(); diff --git a/src/core/server/types.ts b/src/core/server/types.ts index ab1d6c6d95d0..be07a3cfb1fd 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -39,6 +39,5 @@ export type { } from './saved_objects/types'; export type { DomainDeprecationDetails, DeprecationsGetResponse } from './deprecations/types'; export * from './ui_settings/types'; -export * from './legacy/types'; export type { EnvironmentMode, PackageInfo } from '@kbn/config'; export type { ExternalUrlConfig, IExternalUrlPolicy } from './external_url'; diff --git a/src/core/server/ui_settings/integration_tests/doc_exists.ts b/src/core/server/ui_settings/integration_tests/doc_exists.ts index 86a9a24fab6d..59c27cc13617 100644 --- a/src/core/server/ui_settings/integration_tests/doc_exists.ts +++ b/src/core/server/ui_settings/integration_tests/doc_exists.ts @@ -9,10 +9,10 @@ import { getServices, chance } from './lib'; export const docExistsSuite = (savedObjectsIndex: string) => () => { - async function setup(options: any = {}) { + async function setup(options: { initialSettings?: Record } = {}) { const { initialSettings } = options; - const { kbnServer, uiSettings, callCluster } = getServices(); + const { uiSettings, callCluster, supertest } = getServices(); // delete the kibana index to ensure we start fresh await callCluster('deleteByQuery', { @@ -21,31 +21,30 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { conflicts: 'proceed', query: { match_all: {} }, }, + refresh: true, + wait_for_completion: true, }); if (initialSettings) { await uiSettings.setMany(initialSettings); } - return { kbnServer, uiSettings }; + return { uiSettings, supertest }; } describe('get route', () => { it('returns a 200 and includes userValues', async () => { const defaultIndex = chance.word({ length: 10 }); - const { kbnServer } = await setup({ + + const { supertest } = await setup({ initialSettings: { defaultIndex, }, }); - const { statusCode, result } = await kbnServer.inject({ - method: 'GET', - url: '/api/kibana/settings', - }); + const { body } = await supertest('get', '/api/kibana/settings').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -64,20 +63,17 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { describe('set route', () => { it('returns a 200 and all values including update', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { - value: defaultIndex, - }, - }); - expect(statusCode).toBe(200); + const { body } = await supertest('post', '/api/kibana/settings/defaultIndex') + .send({ + value: defaultIndex, + }) + .expect(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -94,18 +90,15 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { }); it('returns a 400 if trying to set overridden value', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/foo', - payload: { + const { body } = await supertest('delete', '/api/kibana/settings/foo') + .send({ value: 'baz', - }, - }); + }) + .expect(400); - expect(statusCode).toBe(400); - expect(result).toEqual({ + expect(body).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, @@ -115,22 +108,18 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { describe('setMany route', () => { it('returns a 200 and all values including updates', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { + const { body } = await supertest('post', '/api/kibana/settings') + .send({ changes: { defaultIndex, }, - }, - }); + }) + .expect(200); - expect(statusCode).toBe(200); - - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -147,20 +136,17 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { }); it('returns a 400 if trying to set overridden value', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { + const { body } = await supertest('post', '/api/kibana/settings') + .send({ changes: { foo: 'baz', }, - }, - }); + }) + .expect(400); - expect(statusCode).toBe(400); - expect(result).toEqual({ + expect(body).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, @@ -172,19 +158,15 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { it('returns a 200 and deletes the setting', async () => { const defaultIndex = chance.word({ length: 10 }); - const { kbnServer, uiSettings } = await setup({ + const { uiSettings, supertest } = await setup({ initialSettings: { defaultIndex }, }); expect(await uiSettings.get('defaultIndex')).toBe(defaultIndex); - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/defaultIndex', - }); + const { body } = await supertest('delete', '/api/kibana/settings/defaultIndex').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -197,15 +179,11 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { }); }); it('returns a 400 if deleting overridden value', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/foo', - }); + const { body } = await supertest('delete', '/api/kibana/settings/foo').expect(400); - expect(statusCode).toBe(400); - expect(result).toEqual({ + expect(body).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, diff --git a/src/core/server/ui_settings/integration_tests/doc_missing.ts b/src/core/server/ui_settings/integration_tests/doc_missing.ts index 9fa3e4c1cfe7..29d1daf3b203 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing.ts @@ -11,14 +11,7 @@ import { getServices, chance } from './lib'; export const docMissingSuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { - const { kbnServer, callCluster } = getServices(); - - // write a setting to ensure kibana index is created - await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: 'abc' }, - }); + const { callCluster } = getServices(); // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { @@ -31,15 +24,11 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('get route', () => { it('creates doc, returns a 200 with settings', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); - const { statusCode, result } = await kbnServer.inject({ - method: 'GET', - url: '/api/kibana/settings', - }); + const { body } = await supertest('get', '/api/kibana/settings').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -55,17 +44,17 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('set route', () => { it('creates doc, returns a 200 with value set', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: defaultIndex }, - }); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + const { body } = await supertest('post', '/api/kibana/settings/defaultIndex') + .send({ + value: defaultIndex, + }) + .expect(200); + + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -84,19 +73,17 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('setMany route', () => { it('creates doc, returns 200 with updated values', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { + + const { body } = await supertest('post', '/api/kibana/settings') + .send({ changes: { defaultIndex }, - }, - }); + }) + .expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -115,15 +102,11 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('delete route', () => { it('creates doc, returns a 200 with just buildNum', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/defaultIndex', - }); + const { body } = await supertest('delete', '/api/kibana/settings/defaultIndex').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), diff --git a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts deleted file mode 100644 index 78fdab7eb8c5..000000000000 --- a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getServices, chance } from './lib'; - -export const docMissingAndIndexReadOnlySuite = (savedObjectsIndex: string) => () => { - // ensure the kibana index has no documents - beforeEach(async () => { - const { kbnServer, callCluster } = getServices(); - - // write a setting to ensure kibana index is created - await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: 'abc' }, - }); - - // delete all docs from kibana index to ensure savedConfig is not found - await callCluster('deleteByQuery', { - index: savedObjectsIndex, - body: { - query: { match_all: {} }, - }, - }); - - // set the index to read only - await callCluster('indices.putSettings', { - index: savedObjectsIndex, - body: { - index: { - blocks: { - read_only: true, - }, - }, - }, - }); - }); - - afterEach(async () => { - const { callCluster } = getServices(); - - // disable the read only block - await callCluster('indices.putSettings', { - index: savedObjectsIndex, - body: { - index: { - blocks: { - read_only: false, - }, - }, - }, - }); - }); - - describe('get route', () => { - it('returns simulated doc with buildNum', async () => { - const { kbnServer } = getServices(); - - const { statusCode, result } = await kbnServer.inject({ - method: 'GET', - url: '/api/kibana/settings', - }); - - expect(statusCode).toBe(200); - - expect(result).toMatchObject({ - settings: { - buildNum: { - userValue: expect.any(Number), - }, - foo: { - userValue: 'bar', - isOverridden: true, - }, - }, - }); - }); - }); - - describe('set route', () => { - it('fails with 403 forbidden', async () => { - const { kbnServer } = getServices(); - - const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: defaultIndex }, - }); - - expect(statusCode).toBe(403); - - expect(result).toEqual({ - error: 'Forbidden', - message: expect.stringContaining('index read-only'), - statusCode: 403, - }); - }); - }); - - describe('setMany route', () => { - it('fails with 403 forbidden', async () => { - const { kbnServer } = getServices(); - - const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { - changes: { defaultIndex }, - }, - }); - - expect(statusCode).toBe(403); - expect(result).toEqual({ - error: 'Forbidden', - message: expect.stringContaining('index read-only'), - statusCode: 403, - }); - }); - }); - - describe('delete route', () => { - it('fails with 403 forbidden', async () => { - const { kbnServer } = getServices(); - - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/defaultIndex', - }); - - expect(statusCode).toBe(403); - expect(result).toEqual({ - error: 'Forbidden', - message: expect.stringContaining('index read-only'), - statusCode: 403, - }); - }); - }); -}; diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index 6e6c357e6ccc..6c7cdfa43cf5 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -12,7 +12,6 @@ import { getEnvOptions } from '@kbn/config/target/mocks'; import { startServers, stopServers } from './lib'; import { docExistsSuite } from './doc_exists'; import { docMissingSuite } from './doc_missing'; -import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; const savedObjectIndex = `.kibana_${kibanaVersion}_001`; @@ -23,7 +22,6 @@ describe('uiSettings/routes', function () { beforeAll(startServers); /* eslint-disable jest/valid-describe */ describe('doc missing', docMissingSuite(savedObjectIndex)); - describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite(savedObjectIndex)); describe('doc exists', docExistsSuite(savedObjectIndex)); /* eslint-enable jest/valid-describe */ afterAll(stopServers); diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index 87176bed5de1..d019dc640f38 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type supertest from 'supertest'; import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/server'; import { @@ -13,6 +14,8 @@ import { TestElasticsearchUtils, TestKibanaUtils, TestUtils, + HttpMethod, + getSupertest, } from '../../../../test_helpers/kbn_server'; import { LegacyAPICaller } from '../../../elasticsearch/'; import { httpServerMock } from '../../../http/http_server.mocks'; @@ -21,13 +24,11 @@ let servers: TestUtils; let esServer: TestElasticsearchUtils; let kbn: TestKibanaUtils; -let kbnServer: TestKibanaUtils['kbnServer']; - interface AllServices { - kbnServer: TestKibanaUtils['kbnServer']; savedObjectsClient: SavedObjectsClientContract; callCluster: LegacyAPICaller; uiSettings: IUiSettingsClient; + supertest: (method: HttpMethod, path: string) => supertest.Test; } let services: AllServices; @@ -47,7 +48,6 @@ export async function startServers() { }); esServer = await servers.startES(); kbn = await servers.startKibana(); - kbnServer = kbn.kbnServer; } export function getServices() { @@ -61,12 +61,10 @@ export function getServices() { httpServerMock.createKibanaRequest() ); - const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient( - savedObjectsClient - ); + const uiSettings = kbn.coreStart.uiSettings.asScopedToClient(savedObjectsClient); services = { - kbnServer, + supertest: (method: HttpMethod, path: string) => getSupertest(kbn.root, method, path), callCluster, savedObjectsClient, uiSettings, @@ -77,7 +75,6 @@ export function getServices() { export async function stopServers() { services = null!; - kbnServer = null!; if (servers) { await esServer.stop(); await kbn.stop(); diff --git a/src/core/server/ui_settings/saved_objects/migrations.test.ts b/src/core/server/ui_settings/saved_objects/migrations.test.ts index cf96372bd20b..cb10f9c7fd98 100644 --- a/src/core/server/ui_settings/saved_objects/migrations.test.ts +++ b/src/core/server/ui_settings/saved_objects/migrations.test.ts @@ -76,6 +76,23 @@ describe('ui_settings 7.12.0 migrations', () => { const migrated = migration(doc); expect(JSON.parse(migrated.attributes['timepicker:quickRanges'])).toEqual([migratedQuickRange]); }); + + // https://github.com/elastic/kibana/issues/95616 + test('returns doc when "timepicker:quickRanges" is null', () => { + const doc = { + type: 'config', + id: '8.0.0', + attributes: { + buildNum: 9007199254740991, + 'timepicker:quickRanges': null, + }, + references: [], + updated_at: '2020-06-09T20:18:20.349Z', + migrationVersion: {}, + }; + const migrated = migration(doc); + expect(migrated).toEqual(doc); + }); }); describe('ui_settings 7.13.0 migrations', () => { diff --git a/src/core/server/ui_settings/saved_objects/migrations.ts b/src/core/server/ui_settings/saved_objects/migrations.ts index 16f217352b99..b187c5f86dab 100644 --- a/src/core/server/ui_settings/saved_objects/migrations.ts +++ b/src/core/server/ui_settings/saved_objects/migrations.ts @@ -32,7 +32,7 @@ export const migrations = { ...doc, ...(doc.attributes && { attributes: Object.keys(doc.attributes).reduce((acc, key) => { - if (key === 'timepicker:quickRanges' && doc.attributes[key].indexOf('section') > -1) { + if (key === 'timepicker:quickRanges' && doc.attributes[key]?.indexOf('section') > -1) { const ranges = JSON.parse(doc.attributes[key]).map( ({ from, to, display }: { from: string; to: string; display: string }) => { return { diff --git a/src/core/server/utils/index.ts b/src/core/server/utils/index.ts deleted file mode 100644 index b0776c48f3be..000000000000 --- a/src/core/server/utils/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export * from './from_root'; -export * from './package_json'; diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 1844b5de3dc3..950ab5f4392e 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -29,11 +29,10 @@ import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; -import { CoreStart } from 'src/core/server'; +import { InternalCoreSetup, InternalCoreStart } from '../server/internal_types'; import { LegacyAPICaller } from '../server/elasticsearch'; import { CliArgs, Env } from '../server/config'; import { Root } from '../server/root'; -import KbnServer from '../../legacy/server/kbn_server'; export type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put'; @@ -125,14 +124,6 @@ export function createRootWithCorePlugins(settings = {}, cliArgs: Partial ReturnType @@ -164,8 +155,8 @@ export interface TestElasticsearchUtils { export interface TestKibanaUtils { root: Root; - coreStart: CoreStart; - kbnServer: KbnServer; + coreSetup: InternalCoreSetup; + coreStart: InternalCoreStart; stop: () => Promise; } @@ -283,14 +274,12 @@ export function createTestServers({ startKibana: async () => { const root = createRootWithCorePlugins(kbnSettings); - await root.setup(); + const coreSetup = await root.setup(); const coreStart = await root.start(); - const kbnServer = getKbnServer(root); - return { root, - kbnServer, + coreSetup, coreStart, stop: async () => await root.shutdown(), }; diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index f19e379482d3..855962070457 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.project.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js index 738b38ee28bd..bb98498e6d60 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -35,13 +35,13 @@ describe(`enumeratePatterns`, () => { 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app' ); }); - it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts to kibana-security`, () => { + it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts to kibana-security`, () => { const short = 'x-pack/plugins/security_solution'; const actual = enumeratePatterns(REPO_ROOT)(log)(new Map([[short, ['kibana-security']]])); expect( actual[0].includes( - `${short}/public/common/components/exceptions/builder/translations.ts kibana-security` + `${short}/public/common/components/exceptions/edit_exception_modal/translations.ts kibana-security` ) ).toBe(true); }); diff --git a/src/dev/run_check_file_casing.js b/src/dev/run_check_file_casing.ts similarity index 92% rename from src/dev/run_check_file_casing.js rename to src/dev/run_check_file_casing.ts index 0add66dd272c..554aa2418f57 100644 --- a/src/dev/run_check_file_casing.js +++ b/src/dev/run_check_file_casing.ts @@ -11,12 +11,13 @@ import globby from 'globby'; import { REPO_ROOT } from '@kbn/utils'; import { run } from '@kbn/dev-utils'; import { File } from './file'; +// @ts-expect-error precommit hooks aren't migrated to TypeScript yet. import { checkFileCasing } from './precommit_hook/check_file_casing'; run(async ({ log }) => { const paths = await globby('**/*', { cwd: REPO_ROOT, - nodir: true, + onlyFiles: true, gitignore: true, ignore: [ // the gitignore: true option makes sure that we don't diff --git a/src/dev/typescript/project.ts b/src/dev/typescript/project.ts index 04a5de945619..8d92284e4963 100644 --- a/src/dev/typescript/project.ts +++ b/src/dev/typescript/project.ts @@ -7,7 +7,7 @@ */ import { basename, dirname, relative, resolve } from 'path'; -import { memoize } from 'lodash'; + import { IMinimatch, Minimatch } from 'minimatch'; import { REPO_ROOT } from '@kbn/utils'; @@ -26,10 +26,6 @@ function testMatchers(matchers: IMinimatch[], path: string) { return matchers.some((matcher) => matcher.match(path)); } -const parentProjectFactory = memoize(function (parentConfigPath: string) { - return new Project(parentConfigPath); -}); - export class Project { public directory: string; public name: string; @@ -38,7 +34,6 @@ export class Project { private readonly include: IMinimatch[]; private readonly exclude: IMinimatch[]; - private readonly parent?: Project; constructor( public tsConfigPath: string, @@ -46,16 +41,15 @@ export class Project { ) { this.config = parseTsConfig(tsConfigPath); - const { files, include, exclude = [], extends: extendsPath } = this.config as { + const { files, include, exclude = [] } = this.config as { files?: string[]; include?: string[]; exclude?: string[]; - extends?: string; }; if (files || !include) { throw new Error( - `[${tsConfigPath}]: tsconfig.json files in the Kibana repo must use "include" keys and not "files"` + 'tsconfig.json files in the Kibana repo must use "include" keys and not "files"' ); } @@ -64,30 +58,9 @@ export class Project { this.name = options.name || relative(REPO_ROOT, this.directory) || basename(this.directory); this.include = makeMatchers(this.directory, include); this.exclude = makeMatchers(this.directory, exclude); - - if (extendsPath !== undefined) { - const parentConfigPath = resolve(this.directory, extendsPath); - this.parent = parentProjectFactory(parentConfigPath); - } - } - - public isAbsolutePathSelected(path: string): boolean { - return this.isExcluded(path) ? false : this.isIncluded(path); } - public isExcluded(path: string): boolean { - if (testMatchers(this.exclude, path)) return true; - if (this.parent) { - return this.parent.isExcluded(path); - } - return false; - } - - public isIncluded(path: string): boolean { - if (testMatchers(this.include, path)) return true; - if (this.parent) { - return this.parent.isIncluded(path); - } - return false; + public isAbsolutePathSelected(path: string) { + return testMatchers(this.exclude, path) ? false : testMatchers(this.include, path); } } diff --git a/src/legacy/server/config/__snapshots__/config.test.js.snap b/src/legacy/server/config/__snapshots__/config.test.js.snap deleted file mode 100644 index 3bf471f8aba2..000000000000 --- a/src/legacy/server/config/__snapshots__/config.test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`lib/config/config class Config() #getDefault(key) array key should throw exception for unknown key 1`] = `"Unknown config key: foo,bar."`; - -exports[`lib/config/config class Config() #getDefault(key) dot notation key should throw exception for unknown key 1`] = `"Unknown config key: foo.bar."`; diff --git a/src/legacy/server/config/config.js b/src/legacy/server/config/config.js deleted file mode 100644 index 81cb0a36333b..000000000000 --- a/src/legacy/server/config/config.js +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Joi from 'joi'; -import { set } from '@elastic/safer-lodash-set'; -import _ from 'lodash'; -import { override } from './override'; -import createDefaultSchema from './schema'; -import { unset, deepCloneWithBuffers as clone, IS_KIBANA_DISTRIBUTABLE } from '../../utils'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { pkg } from '../../../core/server/utils'; -const schema = Symbol('Joi Schema'); -const schemaExts = Symbol('Schema Extensions'); -const vals = Symbol('config values'); - -export class Config { - static withDefaultSchema(settings = {}) { - const defaultSchema = createDefaultSchema(); - return new Config(defaultSchema, settings); - } - - constructor(initialSchema, initialSettings) { - this[schemaExts] = Object.create(null); - this[vals] = Object.create(null); - - this.extendSchema(initialSchema, initialSettings); - } - - extendSchema(extension, settings, key) { - if (!extension) { - return; - } - - if (!key) { - return _.each(extension._inner.children, (child) => { - this.extendSchema(child.schema, _.get(settings, child.key), child.key); - }); - } - - if (this.has(key)) { - throw new Error(`Config schema already has key: ${key}`); - } - - set(this[schemaExts], key, extension); - this[schema] = null; - - this.set(key, settings); - } - - removeSchema(key) { - if (!_.has(this[schemaExts], key)) { - throw new TypeError(`Unknown schema key: ${key}`); - } - - this[schema] = null; - unset(this[schemaExts], key); - unset(this[vals], key); - } - - resetTo(obj) { - this._commit(obj); - } - - set(key, value) { - // clone and modify the config - let config = clone(this[vals]); - if (_.isPlainObject(key)) { - config = override(config, key); - } else { - set(config, key, value); - } - - // attempt to validate the config value - this._commit(config); - } - - _commit(newVals) { - // resolve the current environment - let env = newVals.env; - delete newVals.env; - if (_.isObject(env)) env = env.name; - if (!env) env = 'production'; - - const dev = env === 'development'; - const prod = env === 'production'; - - // pass the environment as context so that it can be refed in config - const context = { - env: env, - prod: prod, - dev: dev, - notProd: !prod, - notDev: !dev, - version: _.get(pkg, 'version'), - branch: _.get(pkg, 'branch'), - buildNum: IS_KIBANA_DISTRIBUTABLE ? pkg.build.number : Number.MAX_SAFE_INTEGER, - buildSha: IS_KIBANA_DISTRIBUTABLE - ? pkg.build.sha - : 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - dist: IS_KIBANA_DISTRIBUTABLE, - }; - - if (!context.dev && !context.prod) { - throw new TypeError( - `Unexpected environment "${env}", expected one of "development" or "production"` - ); - } - - const results = Joi.validate(newVals, this.getSchema(), { - context, - abortEarly: false, - }); - - if (results.error) { - const error = new Error(results.error.message); - error.name = results.error.name; - error.stack = results.error.stack; - throw error; - } - - this[vals] = results.value; - } - - get(key) { - if (!key) { - return clone(this[vals]); - } - - const value = _.get(this[vals], key); - if (value === undefined) { - if (!this.has(key)) { - throw new Error('Unknown config key: ' + key); - } - } - return clone(value); - } - - getDefault(key) { - const schemaKey = Array.isArray(key) ? key.join('.') : key; - - const subSchema = Joi.reach(this.getSchema(), schemaKey); - if (!subSchema) { - throw new Error(`Unknown config key: ${key}.`); - } - - return clone(_.get(Joi.describe(subSchema), 'flags.default')); - } - - has(key) { - function has(key, schema, path) { - path = path || []; - // Catch the partial paths - if (path.join('.') === key) return true; - // Only go deep on inner objects with children - if (_.size(schema._inner.children)) { - for (let i = 0; i < schema._inner.children.length; i++) { - const child = schema._inner.children[i]; - // If the child is an object recurse through it's children and return - // true if there's a match - if (child.schema._type === 'object') { - if (has(key, child.schema, path.concat([child.key]))) return true; - // if the child matches, return true - } else if (path.concat([child.key]).join('.') === key) { - return true; - } - } - } - } - - if (Array.isArray(key)) { - // TODO: add .has() support for array keys - key = key.join('.'); - } - - return !!has(key, this.getSchema()); - } - - getSchema() { - if (!this[schema]) { - this[schema] = (function convertToSchema(children) { - let schema = Joi.object().keys({}).default(); - - for (const key of Object.keys(children)) { - const child = children[key]; - const childSchema = _.isPlainObject(child) ? convertToSchema(child) : child; - - if (!childSchema || !childSchema.isJoi) { - throw new TypeError( - 'Unable to convert configuration definition value to Joi schema: ' + childSchema - ); - } - - schema = schema.keys({ [key]: childSchema }); - } - - return schema; - })(this[schemaExts]); - } - - return this[schema]; - } -} diff --git a/src/legacy/server/config/config.test.js b/src/legacy/server/config/config.test.js deleted file mode 100644 index b617babb8262..000000000000 --- a/src/legacy/server/config/config.test.js +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Config } from './config'; -import _ from 'lodash'; -import Joi from 'joi'; - -/** - * Plugins should defined a config method that takes a joi object. By default - * it should return a way to disallow config - * - * Config should be newed up with a joi schema (containing defaults via joi) - * - * let schema = { ... } - * new Config(schema); - * - */ - -const data = { - test: { - hosts: ['host-01', 'host-02'], - client: { - type: 'datastore', - host: 'store-01', - port: 5050, - }, - }, -}; - -const schema = Joi.object({ - test: Joi.object({ - enable: Joi.boolean().default(true), - hosts: Joi.array().items(Joi.string()), - client: Joi.object({ - type: Joi.string().default('datastore'), - host: Joi.string(), - port: Joi.number(), - }).default(), - undefValue: Joi.string(), - }).default(), -}).default(); - -describe('lib/config/config', function () { - describe('class Config()', function () { - describe('constructor', function () { - it('should not allow any config if the schema is not passed', function () { - const config = new Config(); - const run = function () { - config.set('something.enable', true); - }; - expect(run).toThrow(); - }); - - it('should allow keys in the schema', function () { - const config = new Config(schema); - const run = function () { - config.set('test.client.host', 'http://localhost'); - }; - expect(run).not.toThrow(); - }); - - it('should not allow keys not in the schema', function () { - const config = new Config(schema); - const run = function () { - config.set('paramNotDefinedInTheSchema', true); - }; - expect(run).toThrow(); - }); - - it('should not allow child keys not in the schema', function () { - const config = new Config(schema); - const run = function () { - config.set('test.client.paramNotDefinedInTheSchema', true); - }; - expect(run).toThrow(); - }); - - it('should set defaults', function () { - const config = new Config(schema); - expect(config.get('test.enable')).toBe(true); - expect(config.get('test.client.type')).toBe('datastore'); - }); - }); - - describe('#resetTo(object)', function () { - let config; - beforeEach(function () { - config = new Config(schema); - }); - - it('should reset the config object with new values', function () { - config.set(data); - const newData = config.get(); - newData.test.enable = false; - config.resetTo(newData); - expect(config.get()).toEqual(newData); - }); - }); - - describe('#has(key)', function () { - let config; - beforeEach(function () { - config = new Config(schema); - }); - - it('should return true for fields that exist in the schema', function () { - expect(config.has('test.undefValue')).toBe(true); - }); - - it('should return true for partial objects that exist in the schema', function () { - expect(config.has('test.client')).toBe(true); - }); - - it('should return false for fields that do not exist in the schema', function () { - expect(config.has('test.client.pool')).toBe(false); - }); - }); - - describe('#set(key, value)', function () { - let config; - - beforeEach(function () { - config = new Config(schema); - }); - - it('should use a key and value to set a config value', function () { - config.set('test.enable', false); - expect(config.get('test.enable')).toBe(false); - }); - - it('should use an object to set config values', function () { - const hosts = ['host-01', 'host-02']; - config.set({ test: { enable: false, hosts: hosts } }); - expect(config.get('test.enable')).toBe(false); - expect(config.get('test.hosts')).toEqual(hosts); - }); - - it('should use a flatten object to set config values', function () { - const hosts = ['host-01', 'host-02']; - config.set({ 'test.enable': false, 'test.hosts': hosts }); - expect(config.get('test.enable')).toBe(false); - expect(config.get('test.hosts')).toEqual(hosts); - }); - - it('should override values with just the values present', function () { - const newData = _.cloneDeep(data); - config.set(data); - newData.test.enable = false; - config.set({ test: { enable: false } }); - expect(config.get()).toEqual(newData); - }); - - it('should thow an exception when setting a value with the wrong type', function (done) { - expect.assertions(4); - - const run = function () { - config.set('test.enable', 'something'); - }; - - try { - run(); - } catch (err) { - expect(err).toHaveProperty('name', 'ValidationError'); - expect(err).toHaveProperty( - 'message', - 'child "test" fails because [child "enable" fails because ["enable" must be a boolean]]' - ); - expect(err).not.toHaveProperty('details'); - expect(err).not.toHaveProperty('_object'); - } - - done(); - }); - }); - - describe('#get(key)', function () { - let config; - - beforeEach(function () { - config = new Config(schema); - config.set(data); - }); - - it('should return the whole config object when called without a key', function () { - const newData = _.cloneDeep(data); - newData.test.enable = true; - expect(config.get()).toEqual(newData); - }); - - it('should return the value using dot notation', function () { - expect(config.get('test.enable')).toBe(true); - }); - - it('should return the clone of partial object using dot notation', function () { - expect(config.get('test.client')).not.toBe(data.test.client); - expect(config.get('test.client')).toEqual(data.test.client); - }); - - it('should throw exception for unknown config values', function () { - const run = function () { - config.get('test.does.not.exist'); - }; - expect(run).toThrowError(/Unknown config key: test.does.not.exist/); - }); - - it('should not throw exception for undefined known config values', function () { - const run = function getUndefValue() { - config.get('test.undefValue'); - }; - expect(run).not.toThrow(); - }); - }); - - describe('#getDefault(key)', function () { - let config; - - beforeEach(function () { - config = new Config(schema); - config.set(data); - }); - - describe('dot notation key', function () { - it('should return undefined if there is no default', function () { - const hostDefault = config.getDefault('test.client.host'); - expect(hostDefault).toBeUndefined(); - }); - - it('should return default if specified', function () { - const typeDefault = config.getDefault('test.client.type'); - expect(typeDefault).toBe('datastore'); - }); - - it('should throw exception for unknown key', function () { - expect(() => { - config.getDefault('foo.bar'); - }).toThrowErrorMatchingSnapshot(); - }); - }); - - describe('array key', function () { - it('should return undefined if there is no default', function () { - const hostDefault = config.getDefault(['test', 'client', 'host']); - expect(hostDefault).toBeUndefined(); - }); - - it('should return default if specified', function () { - const typeDefault = config.getDefault(['test', 'client', 'type']); - expect(typeDefault).toBe('datastore'); - }); - - it('should throw exception for unknown key', function () { - expect(() => { - config.getDefault(['foo', 'bar']); - }).toThrowErrorMatchingSnapshot(); - }); - }); - - it('object schema with no default should return default value for property', function () { - const noDefaultSchema = Joi.object() - .keys({ - foo: Joi.array().items(Joi.string().min(1)).default(['bar']), - }) - .required(); - - const config = new Config(noDefaultSchema); - config.set({ - foo: ['baz'], - }); - - const fooDefault = config.getDefault('foo'); - expect(fooDefault).toEqual(['bar']); - }); - - it('should return clone of the default', function () { - const schemaWithArrayDefault = Joi.object() - .keys({ - foo: Joi.array().items(Joi.string().min(1)).default(['bar']), - }) - .default(); - - const config = new Config(schemaWithArrayDefault); - config.set({ - foo: ['baz'], - }); - - expect(config.getDefault('foo')).not.toBe(config.getDefault('foo')); - expect(config.getDefault('foo')).toEqual(config.getDefault('foo')); - }); - }); - - describe('#extendSchema(key, schema)', function () { - let config; - beforeEach(function () { - config = new Config(schema); - }); - - it('should allow you to extend the schema at the top level', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - config.extendSchema(newSchema, {}, 'myTest'); - expect(config.get('myTest.test')).toBe(true); - }); - - it('should allow you to extend the schema with a prefix', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - config.extendSchema(newSchema, {}, 'prefix.myTest'); - expect(config.get('prefix')).toEqual({ myTest: { test: true } }); - expect(config.get('prefix.myTest')).toEqual({ test: true }); - expect(config.get('prefix.myTest.test')).toBe(true); - }); - - it('should NOT allow you to extend the schema if something else is there', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - const run = function () { - config.extendSchema('test', newSchema); - }; - expect(run).toThrow(); - }); - }); - - describe('#removeSchema(key)', function () { - it('should completely remove the key', function () { - const config = new Config( - Joi.object().keys({ - a: Joi.number().default(1), - }) - ); - - expect(config.get('a')).toBe(1); - config.removeSchema('a'); - expect(() => config.get('a')).toThrowError('Unknown config key'); - }); - - it('only removes existing keys', function () { - const config = new Config(Joi.object()); - - expect(() => config.removeSchema('b')).toThrowError('Unknown schema'); - }); - }); - }); -}); diff --git a/src/legacy/server/config/override.test.ts b/src/legacy/server/config/override.test.ts deleted file mode 100644 index d3046eb7bc8a..000000000000 --- a/src/legacy/server/config/override.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { override } from './override'; - -describe('override(target, source)', function () { - it('should override the values form source to target', function () { - const target = { - test: { - enable: true, - host: ['something else'], - client: { - type: 'sql', - }, - }, - }; - - const source = { - test: { - host: ['host-01', 'host-02'], - client: { - type: 'nosql', - }, - foo: { - bar: { - baz: 1, - }, - }, - }, - }; - - expect(override(target, source)).toMatchInlineSnapshot(` - Object { - "test": Object { - "client": Object { - "type": "nosql", - }, - "enable": true, - "foo": Object { - "bar": Object { - "baz": 1, - }, - }, - "host": Array [ - "host-01", - "host-02", - ], - }, - } - `); - }); - - it('does not mutate arguments', () => { - const target = { - foo: { - bar: 1, - baz: 1, - }, - }; - - const source = { - foo: { - bar: 2, - }, - box: 2, - }; - - expect(override(target, source)).toMatchInlineSnapshot(` - Object { - "box": 2, - "foo": Object { - "bar": 2, - "baz": 1, - }, - } - `); - expect(target).not.toHaveProperty('box'); - expect(source.foo).not.toHaveProperty('baz'); - }); - - it('explodes keys with dots in them', () => { - const target = { - foo: { - bar: 1, - }, - 'baz.box.boot.bar.bar': 20, - }; - - const source = { - 'foo.bar': 2, - 'baz.box.boot': { - 'bar.foo': 10, - }, - }; - - expect(override(target, source)).toMatchInlineSnapshot(` - Object { - "baz": Object { - "box": Object { - "boot": Object { - "bar": Object { - "bar": 20, - "foo": 10, - }, - }, - }, - }, - "foo": Object { - "bar": 2, - }, - } - `); - }); -}); diff --git a/src/legacy/server/config/override.ts b/src/legacy/server/config/override.ts deleted file mode 100644 index 55147c955539..000000000000 --- a/src/legacy/server/config/override.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const isObject = (v: any): v is Record => - typeof v === 'object' && v !== null && !Array.isArray(v); - -const assignDeep = (target: Record, source: Record) => { - for (let [key, value] of Object.entries(source)) { - // unwrap dot-separated keys - if (key.includes('.')) { - const [first, ...others] = key.split('.'); - key = first; - value = { [others.join('.')]: value }; - } - - if (isObject(value)) { - if (!target.hasOwnProperty(key)) { - target[key] = {}; - } - - assignDeep(target[key], value); - } else { - target[key] = value; - } - } -}; - -export const override = (...sources: Array>): Record => { - const result = {}; - - for (const object of sources) { - assignDeep(result, object); - } - - return result; -}; diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js deleted file mode 100644 index 81fdfe04290d..000000000000 --- a/src/legacy/server/config/schema.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Joi from 'joi'; -import os from 'os'; -import { legacyLoggingConfigSchema } from '@kbn/legacy-logging'; - -const HANDLED_IN_NEW_PLATFORM = Joi.any().description( - 'This key is handled in the new platform ONLY' -); -export default () => - Joi.object({ - elastic: Joi.object({ - apm: HANDLED_IN_NEW_PLATFORM, - }).default(), - - pkg: Joi.object({ - version: Joi.string().default(Joi.ref('$version')), - branch: Joi.string().default(Joi.ref('$branch')), - buildNum: Joi.number().default(Joi.ref('$buildNum')), - buildSha: Joi.string().default(Joi.ref('$buildSha')), - }).default(), - - env: Joi.object({ - name: Joi.string().default(Joi.ref('$env')), - dev: Joi.boolean().default(Joi.ref('$dev')), - prod: Joi.boolean().default(Joi.ref('$prod')), - }).default(), - - dev: HANDLED_IN_NEW_PLATFORM, - pid: HANDLED_IN_NEW_PLATFORM, - csp: HANDLED_IN_NEW_PLATFORM, - - server: Joi.object({ - name: Joi.string().default(os.hostname()), - // keep them for BWC, remove when not used in Legacy. - // validation should be in sync with one in New platform. - // https://github.com/elastic/kibana/blob/master/src/core/server/http/http_config.ts - basePath: Joi.string() - .default('') - .allow('') - .regex(/(^$|^\/.*[^\/]$)/, `start with a slash, don't end with one`), - host: Joi.string().hostname().default('localhost'), - port: Joi.number().default(5601), - rewriteBasePath: Joi.boolean().when('basePath', { - is: '', - then: Joi.default(false).valid(false), - otherwise: Joi.default(false), - }), - - autoListen: HANDLED_IN_NEW_PLATFORM, - cors: HANDLED_IN_NEW_PLATFORM, - customResponseHeaders: HANDLED_IN_NEW_PLATFORM, - keepaliveTimeout: HANDLED_IN_NEW_PLATFORM, - maxPayloadBytes: HANDLED_IN_NEW_PLATFORM, - publicBaseUrl: HANDLED_IN_NEW_PLATFORM, - socketTimeout: HANDLED_IN_NEW_PLATFORM, - ssl: HANDLED_IN_NEW_PLATFORM, - compression: HANDLED_IN_NEW_PLATFORM, - uuid: HANDLED_IN_NEW_PLATFORM, - xsrf: HANDLED_IN_NEW_PLATFORM, - }).default(), - - uiSettings: HANDLED_IN_NEW_PLATFORM, - - logging: legacyLoggingConfigSchema, - - ops: Joi.object({ - interval: Joi.number().default(5000), - cGroupOverrides: HANDLED_IN_NEW_PLATFORM, - }).default(), - - plugins: HANDLED_IN_NEW_PLATFORM, - path: HANDLED_IN_NEW_PLATFORM, - stats: HANDLED_IN_NEW_PLATFORM, - status: HANDLED_IN_NEW_PLATFORM, - map: HANDLED_IN_NEW_PLATFORM, - i18n: HANDLED_IN_NEW_PLATFORM, - - // temporarily moved here from the (now deleted) kibana legacy plugin - kibana: Joi.object({ - enabled: Joi.boolean().default(true), - index: Joi.string().default('.kibana'), - autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000), - // TODO Also allow units here like in elasticsearch config once this is moved to the new platform - autocompleteTimeout: Joi.number().integer().min(1).default(1000), - }).default(), - - savedObjects: HANDLED_IN_NEW_PLATFORM, - }).default(); diff --git a/src/legacy/server/config/schema.test.js b/src/legacy/server/config/schema.test.js deleted file mode 100644 index c57e6cf9a933..000000000000 --- a/src/legacy/server/config/schema.test.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import schemaProvider from './schema'; -import Joi from 'joi'; - -describe('Config schema', function () { - let schema; - beforeEach(async () => (schema = await schemaProvider())); - - function validate(data, options) { - return Joi.validate(data, schema, options); - } - - describe('server', function () { - it('everything is optional', function () { - const { error } = validate({}); - expect(error).toBe(null); - }); - - describe('basePath', function () { - it('accepts empty strings', function () { - const { error, value } = validate({ server: { basePath: '' } }); - expect(error).toBe(null); - expect(value.server.basePath).toBe(''); - }); - - it('accepts strings with leading slashes', function () { - const { error, value } = validate({ server: { basePath: '/path' } }); - expect(error).toBe(null); - expect(value.server.basePath).toBe('/path'); - }); - - it('rejects strings with trailing slashes', function () { - const { error } = validate({ server: { basePath: '/path/' } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']); - }); - - it('rejects strings without leading slashes', function () { - const { error } = validate({ server: { basePath: 'path' } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']); - }); - - it('rejects things that are not strings', function () { - for (const value of [1, true, {}, [], /foo/]) { - const { error } = validate({ server: { basePath: value } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']); - } - }); - }); - - describe('rewriteBasePath', function () { - it('defaults to false', () => { - const { error, value } = validate({}); - expect(error).toBe(null); - expect(value.server.rewriteBasePath).toBe(false); - }); - - it('accepts false', function () { - const { error, value } = validate({ server: { rewriteBasePath: false } }); - expect(error).toBe(null); - expect(value.server.rewriteBasePath).toBe(false); - }); - - it('accepts true if basePath set', function () { - const { error, value } = validate({ server: { basePath: '/foo', rewriteBasePath: true } }); - expect(error).toBe(null); - expect(value.server.rewriteBasePath).toBe(true); - }); - - it('rejects true if basePath not set', function () { - const { error } = validate({ server: { rewriteBasePath: true } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']); - }); - - it('rejects strings', function () { - const { error } = validate({ server: { rewriteBasePath: 'foo' } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']); - }); - }); - }); -}); diff --git a/src/legacy/server/core/index.ts b/src/legacy/server/core/index.ts deleted file mode 100644 index 2bdd9f26b2c2..000000000000 --- a/src/legacy/server/core/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Server } from '@hapi/hapi'; -import KbnServer from '../kbn_server'; - -/** - * Exposes `kbnServer.newPlatform` through Hapi API. - * @param kbnServer KbnServer singleton instance. - * @param server Hapi server instance to expose `core` on. - */ -export function coreMixin(kbnServer: KbnServer, server: Server) { - // we suppress type error because hapi expect a function here not an object - server.decorate('server', 'newPlatform', kbnServer.newPlatform as any); -} diff --git a/src/legacy/server/http/index.js b/src/legacy/server/http/index.js deleted file mode 100644 index 0fb51b341c3d..000000000000 --- a/src/legacy/server/http/index.js +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { format } from 'url'; -import Boom from '@hapi/boom'; - -export default async function (kbnServer, server) { - server = kbnServer.server; - - const getBasePath = (request) => kbnServer.newPlatform.setup.core.http.basePath.get(request); - - server.route({ - method: 'GET', - path: '/{p*}', - handler: function (req, h) { - const path = req.path; - if (path === '/' || path.charAt(path.length - 1) !== '/') { - throw Boom.notFound(); - } - const basePath = getBasePath(req); - const pathPrefix = basePath ? `${basePath}/` : ''; - return h - .redirect( - format({ - search: req.url.search, - pathname: pathPrefix + path.slice(0, -1), - }) - ) - .permanent(true); - }, - }); -} diff --git a/src/legacy/server/jest.config.js b/src/legacy/server/jest.config.js deleted file mode 100644 index 0a7322d2985f..000000000000 --- a/src/legacy/server/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/legacy/server'], -}; diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts deleted file mode 100644 index 3fe0f5899668..000000000000 --- a/src/legacy/server/kbn_server.d.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Server } from '@hapi/hapi'; - -import { - CoreSetup, - CoreStart, - EnvironmentMode, - LoggerFactory, - PackageInfo, - LegacyServiceSetupDeps, -} from '../../core/server'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LegacyConfig } from '../../core/server/legacy'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { UiPlugins } from '../../core/server/plugins'; - -// lot of legacy code was assuming this type only had these two methods -export type KibanaConfig = Pick; - -// Extend the defaults with the plugins and server methods we need. -declare module 'hapi' { - interface PluginProperties { - spaces: any; - } - - interface Server { - config: () => KibanaConfig; - newPlatform: KbnServer['newPlatform']; - } -} - -type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void; - -export interface PluginsSetup { - [key: string]: object; -} - -export interface KibanaCore { - __internals: { - hapiServer: LegacyServiceSetupDeps['core']['http']['server']; - rendering: LegacyServiceSetupDeps['core']['rendering']; - uiPlugins: UiPlugins; - }; - env: { - mode: Readonly; - packageInfo: Readonly; - }; - setupDeps: { - core: CoreSetup; - plugins: PluginsSetup; - }; - startDeps: { - core: CoreStart; - plugins: Record; - }; - logger: LoggerFactory; -} - -export interface NewPlatform { - __internals: KibanaCore['__internals']; - env: KibanaCore['env']; - coreContext: { - logger: KibanaCore['logger']; - }; - setup: KibanaCore['setupDeps']; - start: KibanaCore['startDeps']; - stop: null; -} - -// eslint-disable-next-line import/no-default-export -export default class KbnServer { - public readonly newPlatform: NewPlatform; - public server: Server; - public inject: Server['inject']; - - constructor(settings: Record, config: KibanaConfig, core: KibanaCore); - - public ready(): Promise; - public mixin(...fns: KbnMixinFunc[]): Promise; - public listen(): Promise; - public close(): Promise; - public applyLoggingConfiguration(settings: any): void; - public config: KibanaConfig; -} - -// Re-export commonly used hapi types. -export { Server, Request, ResponseToolkit } from '@hapi/hapi'; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js deleted file mode 100644 index 4bc76b6a7706..000000000000 --- a/src/legacy/server/kbn_server.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { constant, once, compact, flatten } from 'lodash'; -import { reconfigureLogging } from '@kbn/legacy-logging'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { fromRoot, pkg } from '../../core/server/utils'; -import { Config } from './config'; -import httpMixin from './http'; -import { coreMixin } from './core'; -import { loggingMixin } from './logging'; - -/** - * @typedef {import('./kbn_server').KibanaConfig} KibanaConfig - * @typedef {import('./kbn_server').KibanaCore} KibanaCore - * @typedef {import('./kbn_server').LegacyPlugins} LegacyPlugins - */ - -const rootDir = fromRoot('.'); - -export default class KbnServer { - /** - * @param {Record} settings - * @param {KibanaConfig} config - * @param {KibanaCore} core - */ - constructor(settings, config, core) { - this.name = pkg.name; - this.version = pkg.version; - this.build = pkg.build || false; - this.rootDir = rootDir; - this.settings = settings || {}; - this.config = config; - - const { setupDeps, startDeps, logger, __internals, env } = core; - - this.server = __internals.hapiServer; - this.newPlatform = { - env: { - mode: env.mode, - packageInfo: env.packageInfo, - }, - __internals, - coreContext: { - logger, - }, - setup: setupDeps, - start: startDeps, - stop: null, - }; - - this.ready = constant( - this.mixin( - // Sets global HTTP behaviors - httpMixin, - - coreMixin, - - loggingMixin - ) - ); - - this.listen = once(this.listen); - } - - /** - * Extend the KbnServer outside of the constraints of a plugin. This allows access - * to APIs that are not exposed (intentionally) to the plugins and should only - * be used when the code will be kept up to date with Kibana. - * - * @param {...function} - functions that should be called to mixin functionality. - * They are called with the arguments (kibana, server, config) - * and can return a promise to delay execution of the next mixin - * @return {Promise} - promise that is resolved when the final mixin completes. - */ - async mixin(...fns) { - for (const fn of compact(flatten(fns))) { - await fn.call(this, this, this.server, this.config); - } - } - - /** - * Tell the server to listen for incoming requests, or get - * a promise that will be resolved once the server is listening. - * - * @return undefined - */ - async listen() { - await this.ready(); - - const { server } = this; - - if (process.env.isDevCliChild) { - // help parent process know when we are ready - process.send(['SERVER_LISTENING']); - } - - return server; - } - - async close() { - if (!this.server) { - return; - } - - await this.server.stop(); - } - - async inject(opts) { - if (!this.server) { - await this.ready(); - } - - return await this.server.inject(opts); - } - - applyLoggingConfiguration(settings) { - const config = Config.withDefaultSchema(settings); - - const loggingConfig = config.get('logging'); - const opsConfig = config.get('ops'); - - reconfigureLogging(this.server, loggingConfig, opsConfig.interval); - } -} diff --git a/src/legacy/server/logging/index.js b/src/legacy/server/logging/index.js deleted file mode 100644 index 1b2ae59f4aa0..000000000000 --- a/src/legacy/server/logging/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { setupLogging, setupLoggingRotate } from '@kbn/legacy-logging'; - -export async function loggingMixin(kbnServer, server, config) { - const loggingConfig = config.get('logging'); - const opsInterval = config.get('ops.interval'); - - await setupLogging(server, loggingConfig, opsInterval); - await setupLoggingRotate(server, loggingConfig); -} diff --git a/src/legacy/utils/artifact_type.ts b/src/legacy/utils/artifact_type.ts deleted file mode 100644 index 8243b78b1502..000000000000 --- a/src/legacy/utils/artifact_type.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pkg } from '../../core/server/utils'; -export const IS_KIBANA_DISTRIBUTABLE = pkg.build && pkg.build.distributable === true; -export const IS_KIBANA_RELEASE = pkg.build && pkg.build.release === true; diff --git a/src/legacy/utils/deep_clone_with_buffers.test.ts b/src/legacy/utils/deep_clone_with_buffers.test.ts deleted file mode 100644 index f23e0c849649..000000000000 --- a/src/legacy/utils/deep_clone_with_buffers.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { deepCloneWithBuffers } from './deep_clone_with_buffers'; - -describe('deepCloneWithBuffers()', () => { - it('deep clones objects', () => { - const source = { - a: { - b: {}, - c: {}, - d: [ - { - e: 'f', - }, - ], - }, - }; - - const output = deepCloneWithBuffers(source); - - expect(source.a).toEqual(output.a); - expect(source.a).not.toBe(output.a); - - expect(source.a.b).toEqual(output.a.b); - expect(source.a.b).not.toBe(output.a.b); - - expect(source.a.c).toEqual(output.a.c); - expect(source.a.c).not.toBe(output.a.c); - - expect(source.a.d).toEqual(output.a.d); - expect(source.a.d).not.toBe(output.a.d); - - expect(source.a.d[0]).toEqual(output.a.d[0]); - expect(source.a.d[0]).not.toBe(output.a.d[0]); - }); - - it('copies buffers but keeps them buffers', () => { - const input = Buffer.from('i am a teapot', 'utf8'); - const output = deepCloneWithBuffers(input); - - expect(Buffer.isBuffer(input)).toBe(true); - expect(Buffer.isBuffer(output)).toBe(true); - expect(Buffer.compare(output, input)); - expect(output).not.toBe(input); - }); - - it('copies buffers that are deep', () => { - const input = { - a: { - b: { - c: Buffer.from('i am a teapot', 'utf8'), - }, - }, - }; - const output = deepCloneWithBuffers(input); - - expect(Buffer.isBuffer(input.a.b.c)).toBe(true); - expect(Buffer.isBuffer(output.a.b.c)).toBe(true); - expect(Buffer.compare(output.a.b.c, input.a.b.c)); - expect(output.a.b.c).not.toBe(input.a.b.c); - }); -}); diff --git a/src/legacy/utils/deep_clone_with_buffers.ts b/src/legacy/utils/deep_clone_with_buffers.ts deleted file mode 100644 index c81a572326e7..000000000000 --- a/src/legacy/utils/deep_clone_with_buffers.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { cloneDeepWith } from 'lodash'; - -// We should add `any` return type to overcome bug in lodash types, customizer -// in lodash 3.* can return `undefined` if cloning is handled by the lodash, but -// type of the customizer function doesn't expect that. -function cloneBuffersCustomizer(val: unknown): any { - if (Buffer.isBuffer(val)) { - return Buffer.from(val); - } -} - -export function deepCloneWithBuffers(val: T): T { - return cloneDeepWith(val, cloneBuffersCustomizer); -} diff --git a/src/legacy/utils/index.d.ts b/src/legacy/utils/index.d.ts deleted file mode 100644 index 92fbd6ce715a..000000000000 --- a/src/legacy/utils/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export function unset(object: object, rawPath: string): void; diff --git a/src/legacy/utils/index.js b/src/legacy/utils/index.js deleted file mode 100644 index a96caeb93aaa..000000000000 --- a/src/legacy/utils/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { deepCloneWithBuffers } from './deep_clone_with_buffers'; -export { unset } from './unset'; -export { IS_KIBANA_DISTRIBUTABLE } from './artifact_type'; -export { IS_KIBANA_RELEASE } from './artifact_type'; diff --git a/src/legacy/utils/jest.config.js b/src/legacy/utils/jest.config.js deleted file mode 100644 index 593c3aec9d0b..000000000000 --- a/src/legacy/utils/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/legacy/utils'], -}; diff --git a/src/legacy/utils/unset.js b/src/legacy/utils/unset.js deleted file mode 100644 index fa9a9cee77a1..000000000000 --- a/src/legacy/utils/unset.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; - -export function unset(object, rawPath) { - if (!object) return; - const path = _.toPath(rawPath); - - switch (path.length) { - case 0: - return; - - case 1: - delete object[rawPath]; - break; - - default: - const leaf = path.pop(); - const parentPath = path.slice(); - const parent = _.get(object, parentPath); - unset(parent, leaf); - if (!_.size(parent)) { - unset(object, parentPath); - } - break; - } -} diff --git a/src/legacy/utils/unset.test.js b/src/legacy/utils/unset.test.js deleted file mode 100644 index 0c521ae04612..000000000000 --- a/src/legacy/utils/unset.test.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { unset } from './unset'; - -describe('unset(obj, key)', function () { - describe('invalid input', function () { - it('should do nothing if not given an object', function () { - const obj = 'hello'; - unset(obj, 'e'); - expect(obj).toBe('hello'); - }); - - it('should do nothing if not given a key', function () { - const obj = { one: 1 }; - unset(obj); - expect(obj).toEqual({ one: 1 }); - }); - - it('should do nothing if given an empty string as a key', function () { - const obj = { one: 1 }; - unset(obj, ''); - expect(obj).toEqual({ one: 1 }); - }); - }); - - describe('shallow removal', function () { - let obj; - - beforeEach(function () { - obj = { one: 1, two: 2, deep: { three: 3, four: 4 } }; - }); - - it('should remove the param using a string key', function () { - unset(obj, 'two'); - expect(obj).toEqual({ one: 1, deep: { three: 3, four: 4 } }); - }); - - it('should remove the param using an array key', function () { - unset(obj, ['two']); - expect(obj).toEqual({ one: 1, deep: { three: 3, four: 4 } }); - }); - }); - - describe('deep removal', function () { - let obj; - - beforeEach(function () { - obj = { one: 1, two: 2, deep: { three: 3, four: 4 } }; - }); - - it('should remove the param using a string key', function () { - unset(obj, 'deep.three'); - expect(obj).toEqual({ one: 1, two: 2, deep: { four: 4 } }); - }); - - it('should remove the param using an array key', function () { - unset(obj, ['deep', 'three']); - expect(obj).toEqual({ one: 1, two: 2, deep: { four: 4 } }); - }); - }); - - describe('recursive removal', function () { - it('should clear object if only value is removed', function () { - const obj = { one: { two: { three: 3 } } }; - unset(obj, 'one.two.three'); - expect(obj).toEqual({}); - }); - - it('should clear object if no props are left', function () { - const obj = { one: { two: { three: 3 } } }; - unset(obj, 'one.two'); - expect(obj).toEqual({}); - }); - - it('should remove deep property, then clear the object', function () { - const obj = { one: { two: { three: 3, four: 4 } } }; - unset(obj, 'one.two.three'); - expect(obj).toEqual({ one: { two: { four: 4 } } }); - - unset(obj, 'one.two.four'); - expect(obj).toEqual({}); - }); - }); -}); diff --git a/src/plugins/advanced_settings/tsconfig.json b/src/plugins/advanced_settings/tsconfig.json index 97a855959903..4d62e410326b 100644 --- a/src/plugins/advanced_settings/tsconfig.json +++ b/src/plugins/advanced_settings/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/apm_oss/tsconfig.json b/src/plugins/apm_oss/tsconfig.json index ccb123aaec83..aeb6837c69a9 100644 --- a/src/plugins/apm_oss/tsconfig.json +++ b/src/plugins/apm_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/bfetch/tsconfig.json b/src/plugins/bfetch/tsconfig.json index 6c01479f1929..173ff725d07d 100644 --- a/src/plugins/bfetch/tsconfig.json +++ b/src/plugins/bfetch/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/charts/public/static/components/current_time.tsx b/src/plugins/charts/public/static/components/current_time.tsx index ea4cf1582c7c..9cc261bf3ed8 100644 --- a/src/plugins/charts/public/static/components/current_time.tsx +++ b/src/plugins/charts/public/static/components/current_time.tsx @@ -9,7 +9,7 @@ import moment, { Moment } from 'moment'; import React, { FC } from 'react'; -import { LineAnnotation, AnnotationDomainTypes, LineAnnotationStyle } from '@elastic/charts'; +import { LineAnnotation, AnnotationDomainType, LineAnnotationStyle } from '@elastic/charts'; import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; @@ -46,7 +46,7 @@ export const CurrentTime: FC = ({ isDarkMode, domainEnd }) => diff --git a/src/plugins/charts/tsconfig.json b/src/plugins/charts/tsconfig.json index 99edb2ffe3c1..a4f65d593720 100644 --- a/src/plugins/charts/tsconfig.json +++ b/src/plugins/charts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/console/tsconfig.json b/src/plugins/console/tsconfig.json index d9f49036be8f..34aca5021bac 100644 --- a/src/plugins/console/tsconfig.json +++ b/src/plugins/console/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx index 49b12d46dc9a..cda2f7693062 100644 --- a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx +++ b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx @@ -23,7 +23,7 @@ import { EuiOutsideClickDetector, } from '@elastic/eui'; import { DashboardCopyToCapabilities } from './copy_to_dashboard_action'; -import { DashboardPicker } from '../../services/presentation_util'; +import { LazyDashboardPicker, withSuspense } from '../../services/presentation_util'; import { dashboardCopyToDashboardAction } from '../../dashboard_strings'; import { EmbeddableStateTransfer, IEmbeddable } from '../../services/embeddable'; import { createDashboardEditUrl, DashboardConstants } from '../..'; @@ -37,6 +37,8 @@ interface CopyToDashboardModalProps { closeModal: () => void; } +const DashboardPicker = withSuspense(LazyDashboardPicker); + export function CopyToDashboardModal({ PresentationUtilContext, stateTransfer, diff --git a/src/plugins/dashboard/public/services/presentation_util.ts b/src/plugins/dashboard/public/services/presentation_util.ts index 017b455966f1..d3e6c1ebe9ee 100644 --- a/src/plugins/dashboard/public/services/presentation_util.ts +++ b/src/plugins/dashboard/public/services/presentation_util.ts @@ -6,4 +6,8 @@ * Side Public License, v 1. */ -export { PresentationUtilPluginStart, DashboardPicker } from '../../../presentation_util/public'; +export { + PresentationUtilPluginStart, + LazyDashboardPicker, + withSuspense, +} from '../../../presentation_util/public'; diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 452208b39af6..dd99119cfb45 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/data/common/search/expressions/exists_filter.test.ts b/src/plugins/data/common/search/expressions/exists_filter.test.ts index 60e8a9c7a09c..e3b53b228139 100644 --- a/src/plugins/data/common/search/expressions/exists_filter.test.ts +++ b/src/plugins/data/common/search/expressions/exists_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { existsFilterFunction } from './exists_filter'; diff --git a/src/plugins/data/common/search/expressions/kibana_filter.test.ts b/src/plugins/data/common/search/expressions/kibana_filter.test.ts index 56a9e1ce660c..ac8ae55492cc 100644 --- a/src/plugins/data/common/search/expressions/kibana_filter.test.ts +++ b/src/plugins/data/common/search/expressions/kibana_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { kibanaFilterFunction } from './kibana_filter'; diff --git a/src/plugins/data/common/search/expressions/phrase_filter.test.ts b/src/plugins/data/common/search/expressions/phrase_filter.test.ts index 90e471e166f5..39bd907513a0 100644 --- a/src/plugins/data/common/search/expressions/phrase_filter.test.ts +++ b/src/plugins/data/common/search/expressions/phrase_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { phraseFilterFunction } from './phrase_filter'; diff --git a/src/plugins/data/common/search/expressions/range_filter.test.ts b/src/plugins/data/common/search/expressions/range_filter.test.ts index 129e6bd82e16..92670f8a044b 100644 --- a/src/plugins/data/common/search/expressions/range_filter.test.ts +++ b/src/plugins/data/common/search/expressions/range_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createMockContext } from '../../../../expressions/common/mocks'; +import { createMockContext } from '../../../../expressions/common'; import { functionWrapper } from './utils'; import { rangeFilterFunction } from './range_filter'; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 5639229e1ff3..d2f04228ed39 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -85,7 +85,7 @@ class FilterEditorUI extends Component { public render() { return (
- + { panelPaddingSize="none" repositionOnScroll > - +
- +

- + {savedQueryPopoverTitleText} {savedQueries.length > 0 ? ( @@ -234,7 +234,7 @@ export function SavedQueryManagementComponent({ )} - + , { expressions, usageCollection }: IndexPatternsServiceSetupDeps): void; // (undocumented) start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1232,7 +1232,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index b99a2f6f8590..9c95878af631 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/dev_tools/tsconfig.json b/src/plugins/dev_tools/tsconfig.json index f369396b17fb..c17b2341fd42 100644 --- a/src/plugins/dev_tools/tsconfig.json +++ b/src/plugins/dev_tools/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 96765d76a340..ec98199c3423 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/embeddable/tsconfig.json b/src/plugins/embeddable/tsconfig.json index eacfa831ecee..27a887500fb6 100644 --- a/src/plugins/embeddable/tsconfig.json +++ b/src/plugins/embeddable/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/es_ui_shared/tsconfig.json b/src/plugins/es_ui_shared/tsconfig.json index 3d102daaf3aa..9bcda2e0614d 100644 --- a/src/plugins/es_ui_shared/tsconfig.json +++ b/src/plugins/es_ui_shared/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/expressions/common/mocks.ts b/src/plugins/expressions/common/mocks.ts index 20bdbca07f00..eaeebd8e5349 100644 --- a/src/plugins/expressions/common/mocks.ts +++ b/src/plugins/expressions/common/mocks.ts @@ -34,5 +34,3 @@ export const createMockExecutionContext = ...extraContext, }; }; - -export { createMockContext } from './util/test_utils'; diff --git a/src/plugins/expressions/common/util/index.ts b/src/plugins/expressions/common/util/index.ts index 5f83d962d5ae..470dfc3c2d43 100644 --- a/src/plugins/expressions/common/util/index.ts +++ b/src/plugins/expressions/common/util/index.ts @@ -10,3 +10,4 @@ export * from './create_error'; export * from './get_by_alias'; export * from './tables_adapter'; export * from './expressions_inspector_adapter'; +export * from './test_utils'; diff --git a/src/plugins/expressions/tsconfig.json b/src/plugins/expressions/tsconfig.json index fe76ba3050a3..cce71013cefa 100644 --- a/src/plugins/expressions/tsconfig.json +++ b/src/plugins/expressions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index 19ab5a8e6efe..b15e1fc011b9 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx index 73a4837d6e0c..69092b2bc092 100644 --- a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx @@ -64,14 +64,14 @@ const geti18nTexts = (fieldsToDelete?: string[]) => { typeConfirm: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.typeConfirm', { - defaultMessage: "Type 'REMOVE' to confirm", + defaultMessage: 'Enter REMOVE to confirm', } ), warningRemovingFields: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningRemovingFields', { defaultMessage: - 'Warning: Removing fields may break searches or visualizations that rely on this field.', + 'Removing fields can break searches and visualizations that rely on this field.', } ), }; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx index 486df1a7707a..e0ca654c956c 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -53,20 +53,29 @@ const geti18nTexts = (field?: Field) => { confirmButtonText: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.saveButtonLabel', { - defaultMessage: 'Save', + defaultMessage: 'Save changes', } ), warningChangingFields: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningChangingFields', { defaultMessage: - 'Warning: Changing name or type may break searches or visualizations that rely on this field.', + 'Changing name or type can break searches and visualizations that rely on this field.', } ), typeConfirm: i18n.translate( 'indexPatternFieldEditor.saveRuntimeField.confirmModal.typeConfirm', { - defaultMessage: "Type 'CHANGE' to continue:", + defaultMessage: 'Enter CHANGE to continue', + } + ), + titleConfirmChanges: i18n.translate( + 'indexPatternFieldEditor.saveRuntimeField.confirmModal.title', + { + defaultMessage: `Save changes to '{name}'`, + values: { + name: field?.name, + }, } ), }; @@ -211,7 +220,7 @@ const FieldEditorFlyoutContentComponent = ({ const modal = isModalVisible ? ( = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableInspectEsQueries': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'banners:placement': { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, @@ -428,7 +432,7 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'observability:enableInspectEsQueries': { + 'labs:presentation:unifiedToolbar': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 810f13931225..613ada418c6e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -118,4 +118,5 @@ export interface UsageStats { 'banners:placement': string; 'banners:textColor': string; 'banners:backgroundColor': string; + 'labs:presentation:unifiedToolbar': boolean; } diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index 100f1f03955d..d664d936f666 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/kibana_utils/tsconfig.json b/src/plugins/kibana_utils/tsconfig.json index d9572707e866..ae5e9b90af80 100644 --- a/src/plugins/kibana_utils/tsconfig.json +++ b/src/plugins/kibana_utils/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/legacy_export/tsconfig.json b/src/plugins/legacy_export/tsconfig.json index d6689ea1067d..ec006d492499 100644 --- a/src/plugins/legacy_export/tsconfig.json +++ b/src/plugins/legacy_export/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/management/tsconfig.json b/src/plugins/management/tsconfig.json index 3423299a53df..ba3661666631 100644 --- a/src/plugins/management/tsconfig.json +++ b/src/plugins/management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/maps_ems/public/service_settings/service_settings.test.js b/src/plugins/maps_ems/public/service_settings/service_settings.test.js index 5bd371aace79..eb67997c253b 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings.test.js +++ b/src/plugins/maps_ems/public/service_settings/service_settings.test.js @@ -103,43 +103,8 @@ describe('service_settings (FKA tile_map test)', function () { expect(tmsService.attribution.includes('OpenStreetMap')).toEqual(true); }); - describe('modify - url', function () { - let tilemapServices; - + describe('tms mods', function () { let serviceSettings; - async function assertQuery(expected) { - const attrs = await serviceSettings.getAttributesForTMSLayer(tilemapServices[0]); - const urlObject = url.parse(attrs.url, true); - Object.keys(expected).forEach((key) => { - expect(urlObject.query[key]).toEqual(expected[key]); - }); - } - - it('accepts an object', async () => { - serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar' }); - }); - - it('merged additions with previous values', async () => { - // ensure that changes are always additive - serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); - serviceSettings.setQueryParams({ bar: 'stool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar', bar: 'stool' }); - }); - - it('overwrites conflicting previous values', async () => { - serviceSettings = makeServiceSettings(); - // ensure that conflicts are overwritten - serviceSettings.setQueryParams({ foo: 'bar' }); - serviceSettings.setQueryParams({ bar: 'stool' }); - serviceSettings.setQueryParams({ foo: 'tstool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'tstool', bar: 'stool' }); - }); it('should merge in tilemap url', async () => { serviceSettings = makeServiceSettings( @@ -161,7 +126,7 @@ describe('service_settings (FKA tile_map test)', function () { id: 'road_map', name: 'Road Map - Bright', url: - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3', + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl', minZoom: 0, maxZoom: 10, attribution: @@ -208,19 +173,19 @@ describe('service_settings (FKA tile_map test)', function () { ); expect(desaturationFalse.url).toEqual( - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(desaturationFalse.maxZoom).toEqual(10); expect(desaturationTrue.url).toEqual( - 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(desaturationTrue.maxZoom).toEqual(18); expect(darkThemeDesaturationFalse.url).toEqual( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(darkThemeDesaturationFalse.maxZoom).toEqual(22); expect(darkThemeDesaturationTrue.url).toEqual( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(darkThemeDesaturationTrue.maxZoom).toEqual(22); }); @@ -264,14 +229,13 @@ describe('service_settings (FKA tile_map test)', function () { describe('File layers', function () { it('should load manifest (all props)', async function () { const serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); const fileLayers = await serviceSettings.getFileLayers(); expect(fileLayers.length).toEqual(19); const assertions = fileLayers.map(async function (fileLayer) { expect(fileLayer.origin).toEqual(ORIGIN.EMS); const fileUrl = await serviceSettings.getUrlForRegionLayer(fileLayer); const urlObject = url.parse(fileUrl, true); - Object.keys({ foo: 'bar', elastic_tile_service_tos: 'agree' }).forEach((key) => { + Object.keys({ elastic_tile_service_tos: 'agree' }).forEach((key) => { expect(typeof urlObject.query[key]).toEqual('string'); }); }); diff --git a/src/plugins/maps_ems/public/service_settings/service_settings.ts b/src/plugins/maps_ems/public/service_settings/service_settings.ts index f7c735b6c303..412db42a1570 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings.ts +++ b/src/plugins/maps_ems/public/service_settings/service_settings.ts @@ -22,7 +22,6 @@ export class ServiceSettings implements IServiceSettings { private readonly _mapConfig: MapsEmsConfig; private readonly _tilemapsConfig: TileMapConfig; private readonly _hasTmsConfigured: boolean; - private _showZoomMessage: boolean; private readonly _emsClient: EMSClient; private readonly tmsOptionsFromConfig: any; @@ -31,7 +30,6 @@ export class ServiceSettings implements IServiceSettings { this._tilemapsConfig = tilemapsConfig; this._hasTmsConfigured = typeof tilemapsConfig.url === 'string' && tilemapsConfig.url !== ''; - this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), appVersion: getKibanaVersion(), @@ -45,6 +43,9 @@ export class ServiceSettings implements IServiceSettings { return fetch(...args); }, }); + // any kibana user, regardless of distribution, should get all zoom levels + // use `sspl` license to indicate this + this._emsClient.addQueryParams({ license: 'sspl' }); const markdownIt = new MarkdownIt({ html: false, @@ -58,18 +59,6 @@ export class ServiceSettings implements IServiceSettings { }); } - shouldShowZoomMessage({ origin }: { origin: string }): boolean { - return origin === ORIGIN.EMS && this._showZoomMessage; - } - - enableZoomMessage(): void { - this._showZoomMessage = true; - } - - disableZoomMessage(): void { - this._showZoomMessage = false; - } - __debugStubManifestCalls(manifestRetrieval: () => Promise): { removeStub: () => void } { const oldGetManifest = this._emsClient.getManifest; diff --git a/src/plugins/maps_ems/public/service_settings/service_settings_types.ts b/src/plugins/maps_ems/public/service_settings/service_settings_types.ts index 80a9aae83584..6b04bd200eba 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings_types.ts +++ b/src/plugins/maps_ems/public/service_settings/service_settings_types.ts @@ -46,8 +46,6 @@ export interface IServiceSettings { getFileLayers(): Promise; getUrlForRegionLayer(layer: FileLayer): Promise; setQueryParams(params: { [p: string]: string }): void; - enableZoomMessage(): void; - disableZoomMessage(): void; getAttributesForTMSLayer( tmsServiceConfig: TmsLayer, isDesaturated: boolean, diff --git a/src/plugins/maps_ems/tsconfig.json b/src/plugins/maps_ems/tsconfig.json index 7f44da00d47a..b85c3da66b83 100644 --- a/src/plugins/maps_ems/tsconfig.json +++ b/src/plugins/maps_ems/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index 8e283288e34b..f321274791a3 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -5,5 +5,5 @@ "ui": true, "server": true, "requiredPlugins": ["mapsEms"], - "requiredBundles": ["kibanaReact", "visDefaultEditor", "mapsEms"] + "requiredBundles": ["visDefaultEditor", "mapsEms"] } diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index 9cd574c5246e..a261bcf6edd8 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -193,13 +193,12 @@ export function BaseMapsVisualizationProvider() { isDesaturated, isDarkMode ); - const showZoomMessage = serviceSettings.shouldShowZoomMessage(tmsLayer); const options = { ...tmsLayer }; delete options.id; delete options.subdomains; this._kibanaMap.setBaseLayer({ baseLayerType: 'tms', - options: { ...options, showZoomMessage, ...meta }, + options: { ...options, ...meta }, }); } diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js index eea831541928..62dbbda2588a 100644 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/map/kibana_map.js @@ -7,13 +7,11 @@ */ import { EventEmitter } from 'events'; -import { createZoomWarningMsg } from './map_messages'; import $ from 'jquery'; import { get, isEqual, escape } from 'lodash'; import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; import { ORIGIN } from '../../../maps_ems/common'; -import { getToasts } from '../kibana_services'; import { L } from '../leaflet'; function makeFitControl(fitContainer, kibanaMap) { @@ -479,22 +477,6 @@ export class KibanaMap extends EventEmitter { this._updateLegend(); } - _addMaxZoomMessage = (layer) => { - const zoomWarningMsg = createZoomWarningMsg( - getToasts(), - this.getZoomLevel, - this.getMaxZoomLevel - ); - - this._leafletMap.on('zoomend', zoomWarningMsg); - this._containerNode.setAttribute('data-test-subj', 'zoomWarningEnabled'); - - layer.on('remove', () => { - this._leafletMap.off('zoomend', zoomWarningMsg); - this._containerNode.removeAttribute('data-test-subj'); - }); - }; - setLegendPosition(position) { if (this._legendPosition === position) { if (!this._leafletLegendControl) { @@ -572,11 +554,6 @@ export class KibanaMap extends EventEmitter { }); this._leafletBaseLayer = baseLayer; - if (settings.options.showZoomMessage) { - baseLayer.on('add', () => { - this._addMaxZoomMessage(baseLayer); - }); - } this._leafletBaseLayer.addTo(this._leafletMap); this._leafletBaseLayer.bringToBack(); if (settings.options.minZoom > this._leafletMap.getZoom()) { diff --git a/src/plugins/maps_legacy/public/map/map_messages.js b/src/plugins/maps_legacy/public/map/map_messages.js deleted file mode 100644 index f60d819f0b39..000000000000 --- a/src/plugins/maps_legacy/public/map/map_messages.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; -import { toMountPoint } from '../../../kibana_react/public'; - -export const createZoomWarningMsg = (function () { - let disableZoomMsg = false; - const setZoomMsg = (boolDisableMsg) => (disableZoomMsg = boolDisableMsg); - - class ZoomWarning extends React.Component { - constructor(props) { - super(props); - this.state = { - disabled: false, - }; - } - - render() { - return ( -

-

- - {`default distribution `} - - ), - ems: ( - - {`Elastic Maps Service`} - - ), - wms: ( - - {`Custom WMS Configuration`} - - ), - configSettings: ( - - {`Custom TMS Using Config Settings`} - - ), - }} - /> -

- - { - this.setState( - { - disabled: true, - }, - () => this.props.onChange(this.state.disabled) - ); - }} - data-test-subj="suppressZoomWarnings" - > - {`Don't show again`} - -
- ); - } - } - - const zoomToast = { - title: 'No additional zoom levels', - text: toMountPoint(), - 'data-test-subj': 'maxZoomWarning', - }; - - return (toastService, getZoomLevel, getMaxZoomLevel) => { - return () => { - const zoomLevel = getZoomLevel(); - const maxMapZoom = getMaxZoomLevel(); - if (!disableZoomMsg && zoomLevel === maxMapZoom) { - toastService.addDanger(zoomToast); - } - }; - }; -})(); diff --git a/src/plugins/maps_legacy/tsconfig.json b/src/plugins/maps_legacy/tsconfig.json index c600024cc4a7..f757e35f785a 100644 --- a/src/plugins/maps_legacy/tsconfig.json +++ b/src/plugins/maps_legacy/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/navigation/tsconfig.json b/src/plugins/navigation/tsconfig.json index bb86142e1c44..07cfe10d7d81 100644 --- a/src/plugins/navigation/tsconfig.json +++ b/src/plugins/navigation/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json index 84626b2f3a6a..66244a22336c 100644 --- a/src/plugins/newsfeed/tsconfig.json +++ b/src/plugins/newsfeed/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/presentation_util/common/index.ts b/src/plugins/presentation_util/common/index.ts index 8b556af07dd6..bf8819b13a92 100644 --- a/src/plugins/presentation_util/common/index.ts +++ b/src/plugins/presentation_util/common/index.ts @@ -8,3 +8,5 @@ export const PLUGIN_ID = 'presentationUtil'; export const PLUGIN_NAME = 'presentationUtil'; + +export * from './labs'; diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts new file mode 100644 index 000000000000..65e42996ae91 --- /dev/null +++ b/src/plugins/presentation_util/common/labs.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const UNIFIED_TOOLBAR = 'labs:presentation:unifiedToolbar'; + +export const projectIDs = [UNIFIED_TOOLBAR] as const; +export const environmentNames = ['kibana', 'browser', 'session'] as const; +export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; + +/** + * This is a list of active Labs Projects for the Presentation Team. It is the "source of truth" for all projects + * provided to users of our solutions in Kibana. + */ +export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { + [UNIFIED_TOOLBAR]: { + id: UNIFIED_TOOLBAR, + isActive: false, + environments: ['kibana', 'browser', 'session'], + name: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectName', { + defaultMessage: 'Unified Toolbar', + }), + description: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectDescription', { + defaultMessage: 'Enable the new unified toolbar design for Presentation solutions', + }), + solutions: ['dashboard', 'canvas'], + }, +}; + +export type ProjectID = typeof projectIDs[number]; +export type EnvironmentName = typeof environmentNames[number]; +export type SolutionName = typeof solutionNames[number]; + +export type EnvironmentStatus = { + [env in EnvironmentName]?: boolean; +}; + +export type ProjectStatus = { + defaultValue: boolean; + isEnabled: boolean; + isOverride: boolean; +} & EnvironmentStatus; + +export interface ProjectConfig { + id: ProjectID; + name: string; + isActive: boolean; + environments: EnvironmentName[]; + description: string; + solutions: SolutionName[]; +} + +export type Project = ProjectConfig & { status: ProjectStatus }; + +export const getProjectIDs = () => projectIDs; + +export const isProjectEnabledByStatus = (active: boolean, status: EnvironmentStatus): boolean => { + // If the project is enabled by default, then any false flag will flip the switch, and vice-versa. + return active + ? Object.values(status).every((value) => value === true) + : Object.values(status).some((value) => value === true); +}; diff --git a/src/plugins/presentation_util/kibana.json b/src/plugins/presentation_util/kibana.json index b1b3d768c3e7..c7d272dcd02a 100644 --- a/src/plugins/presentation_util/kibana.json +++ b/src/plugins/presentation_util/kibana.json @@ -2,8 +2,10 @@ "id": "presentationUtil", "version": "1.0.0", "kibanaVersion": "kibana", - "server": false, + "server": true, "ui": true, - "requiredPlugins": ["savedObjects"], + "requiredPlugins": [ + "savedObjects" + ], "optionalPlugins": [] } diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.tsx index d32afca5cede..47ba57076502 100644 --- a/src/plugins/presentation_util/public/components/dashboard_picker.tsx +++ b/src/plugins/presentation_util/public/components/dashboard_picker.tsx @@ -99,3 +99,7 @@ export function DashboardPicker(props: DashboardPickerProps) { /> ); } + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default DashboardPicker; diff --git a/src/plugins/presentation_util/public/components/index.tsx b/src/plugins/presentation_util/public/components/index.tsx new file mode 100644 index 000000000000..af806e1c22f1 --- /dev/null +++ b/src/plugins/presentation_util/public/components/index.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Suspense, ComponentType, ReactElement } from 'react'; +import { EuiLoadingSpinner, EuiErrorBoundary } from '@elastic/eui'; + +/** + * A HOC which supplies React.Suspense with a fallback component, and a `EuiErrorBoundary` to contain errors. + * @param Component A component deferred by `React.lazy` + * @param fallback A fallback component to render while things load; default is `EuiLoadingSpinner` + */ +export const withSuspense =

( + Component: ComponentType

, + fallback: ReactElement | null = +) => (props: P) => ( + + + + + +); + +export const LazyLabsBeakerButton = withSuspense( + React.lazy(() => import('./labs/labs_beaker_button')) +); + +export const LazyLabsFlyout = withSuspense(React.lazy(() => import('./labs/labs_flyout'))); + +export const LazyDashboardPicker = React.lazy(() => import('./dashboard_picker')); + +export const LazySavedObjectSaveModalDashboard = React.lazy( + () => import('./saved_object_save_modal_dashboard') +); diff --git a/src/plugins/presentation_util/public/components/labs/environment_switch.tsx b/src/plugins/presentation_util/public/components/labs/environment_switch.tsx new file mode 100644 index 000000000000..0acdd433cbac --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/environment_switch.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiIconTip, + EuiSpacer, + EuiScreenReaderOnly, +} from '@elastic/eui'; + +import { EnvironmentName } from '../../../common/labs'; +import { LabsStrings } from '../../i18n'; + +const { Switch: strings } = LabsStrings.Components; + +const switchText: { [env in EnvironmentName]: { name: string; help: string } } = { + kibana: strings.getKibanaSwitchText(), + browser: strings.getBrowserSwitchText(), + session: strings.getSessionSwitchText(), +}; + +export interface Props { + env: EnvironmentName; + isChecked: boolean; + onChange: (checked: boolean) => void; + name: string; +} + +export const EnvironmentSwitch = ({ env, isChecked, onChange, name }: Props) => ( + + + + + + {name} - + + {switchText[env].name} + + } + onChange={(e) => onChange(e.target.checked)} + compressed + /> + + + + + + + +); diff --git a/src/plugins/presentation_util/public/components/labs/labs.stories.tsx b/src/plugins/presentation_util/public/components/labs/labs.stories.tsx new file mode 100644 index 000000000000..a9a1a0753d24 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs.stories.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { LabsBeakerButton } from './labs_beaker_button'; +import { LabsFlyout } from './labs_flyout'; + +export default { + title: 'Labs/Flyout', + description: + 'A set of components used for providing Labs controls and projects in another solution.', + argTypes: {}, +}; + +export function BeakerButton() { + return ; +} + +export function Flyout() { + return ; +} + +export function EmptyFlyout() { + return ; +} diff --git a/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx b/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx new file mode 100644 index 000000000000..6d7fd4afdac6 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { EuiButton, EuiIcon, EuiNotificationBadge, EuiButtonProps } from '@elastic/eui'; + +import { pluginServices } from '../../services'; +import { LabsFlyout, Props as FlyoutProps } from './labs_flyout'; + +export type Props = EuiButtonProps & Pick; + +export const LabsBeakerButton = ({ solutions, ...props }: Props) => { + const { labs: labsService } = pluginServices.getHooks(); + const { getProjects } = labsService.useService(); + const [isOpen, setIsOpen] = useState(false); + + const projects = getProjects(); + + const [overrideCount, onEnabledCountChange] = useState( + Object.values(projects).filter((project) => project.status.isOverride).length + ); + + const onButtonClick = () => setIsOpen((open) => !open); + const onClose = () => setIsOpen(false); + + return ( + <> + + + {overrideCount > 0 ? ( + + {overrideCount} + + ) : null} + + {isOpen ? : null} + + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default LabsBeakerButton; diff --git a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx new file mode 100644 index 000000000000..562d3b291a4b --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { ReactNode, useRef, useState, useEffect } from 'react'; +import { + EuiFlyout, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiButton, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, + EuiIcon, +} from '@elastic/eui'; + +import { SolutionName, ProjectStatus, ProjectID, Project, EnvironmentName } from '../../../common'; +import { pluginServices } from '../../services'; +import { LabsStrings } from '../../i18n'; + +import { ProjectList } from './project_list'; + +const { Flyout: strings } = LabsStrings.Components; + +export interface Props { + onClose: () => void; + solutions?: SolutionName[]; + onEnabledCountChange?: (overrideCount: number) => void; +} + +const hasStatusChanged = ( + original: Record, + current: Record +): boolean => { + for (const id of Object.keys(original) as ProjectID[]) { + for (const key of Object.keys(original[id].status) as Array) { + if (original[id].status[key] !== current[id].status[key]) { + return true; + } + } + } + return false; +}; + +export const getOverridenCount = (projects: Record) => + Object.values(projects).filter((project) => project.status.isOverride).length; + +export const LabsFlyout = (props: Props) => { + const { solutions, onEnabledCountChange = () => {}, onClose } = props; + const { labs: labsService } = pluginServices.getHooks(); + const { getProjects, setProjectStatus, reset } = labsService.useService(); + + const [projects, setProjects] = useState(getProjects()); + const [overrideCount, setOverrideCount] = useState(getOverridenCount(projects)); + const initialStatus = useRef(getProjects()); + + const isChanged = hasStatusChanged(initialStatus.current, projects); + + useEffect(() => { + setOverrideCount(getOverridenCount(projects)); + }, [projects]); + + useEffect(() => { + onEnabledCountChange(overrideCount); + }, [onEnabledCountChange, overrideCount]); + + const onStatusChange = (id: ProjectID, env: EnvironmentName, enabled: boolean) => { + setProjectStatus(id, env, enabled); + setProjects(getProjects()); + }; + + let footer: ReactNode = null; + + const resetButton = ( + { + reset(); + setProjects(getProjects()); + }} + isDisabled={!overrideCount} + > + {strings.getResetToDefaultLabel()} + + ); + + const refreshButton = ( + { + window.location.reload(); + }} + isDisabled={!isChanged} + > + {strings.getRefreshLabel()} + + ); + + footer = ( + + + {resetButton} + {refreshButton} + + + ); + + return ( + + + +

+ + + + + {strings.getTitleLabel()} + +

+ + + + + + {footer} + + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default LabsFlyout; diff --git a/src/plugins/presentation_util/public/components/labs/project_list.tsx b/src/plugins/presentation_util/public/components/labs/project_list.tsx new file mode 100644 index 000000000000..4ecf45409b02 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiCallOut } from '@elastic/eui'; + +import { SolutionName, ProjectID, Project } from '../../../common'; +import { ProjectListItem, Props as ProjectListItemProps } from './project_list_item'; + +import { LabsStrings } from '../../i18n'; + +const { List: strings } = LabsStrings.Components; + +export interface Props { + solutions?: SolutionName[]; + projects: Record; + onStatusChange: ProjectListItemProps['onStatusChange']; +} + +const EmptyList = () => ; + +export const ProjectList = (props: Props) => { + const { solutions, projects, onStatusChange } = props; + + const items = Object.values(projects) + .map((project) => { + // Filter out any panels that don't match the solutions filter, (if provided). + if (solutions && !solutions.some((solution) => project.solutions.includes(solution))) { + return null; + } + + return ( +
  • + +
  • + ); + }) + .filter((item) => item !== null); + + return ( + + {items.length > 0 ?
      {items}
    : } +
    + ); +}; diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.scss b/src/plugins/presentation_util/public/components/labs/project_list_item.scss new file mode 100644 index 000000000000..c91a07576b31 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.scss @@ -0,0 +1,46 @@ +.projectListItem { + position: relative; + background: $euiColorEmptyShade; + padding: $euiSizeL; + min-width: 500px; + + &--isOverridden:before { + position: absolute; + top: $euiSizeL; + left: 4px; + bottom: $euiSizeL; + width: 4px; + background: $euiColorPrimary; + content: ''; + } + + .euiSwitch__label { + width: 100%; + } +} + +.projectListItem + .projectListItem:after { + position: absolute; + top: 0; + right: 0; + left: 0; + height: 1px; + background: $euiColorLightShade; + content: ''; +} + +.euiFlyout .projectListItem { + padding: $euiSizeL $euiSizeXS; + + &:first-child { + padding-top: 0; + } + + &--isOverridden:before { + left: -12px; + } + + &--isOverridden:first-child:before { + top: 0; + } +} diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx new file mode 100644 index 000000000000..ce93abded521 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { mapValues } from 'lodash'; + +import { EnvironmentStatus, ProjectConfig, ProjectID, ProjectStatus } from '../../../common'; +import { applyProjectStatus } from '../../services/labs'; +import { ProjectListItem, Props } from './project_list_item'; + +import { projects as projectConfigs } from '../../../common'; +import { ProjectList } from './project_list'; + +export default { + title: 'Labs/ProjectList', + description: 'A set of controls for displaying and manipulating projects.', +}; + +const projects = mapValues(projectConfigs, (project) => + applyProjectStatus(project, { kibana: false, session: false, browser: false }) +); + +export function List() { + return ; +} + +export function EmptyList() { + return ; +} + +export const ListItem = ( + props: Pick< + Props['project'], + 'description' | 'isActive' | 'name' | 'solutions' | 'environments' + > & + Omit +) => { + const { kibana, browser, session, ...rest } = props; + const status: EnvironmentStatus = { kibana, browser, session }; + const projectConfig: ProjectConfig = { + ...rest, + id: 'storybook:component' as ProjectID, + }; + + return ( +
    + ({ ...status, [env]: enabled })} + /> +
    + ); +}; + +ListItem.args = { + isActive: false, + name: 'Demo Project', + description: 'This is a demo project, and this is the description of the demo project.', + kibana: false, + browser: false, + session: false, + solutions: ['dashboard', 'canvas'], + environments: ['kibana', 'browser', 'session'], +}; + +ListItem.argTypes = { + environments: { + control: { + type: 'check', + options: ['kibana', 'browser', 'session'], + }, + }, +}; diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.tsx b/src/plugins/presentation_util/public/components/labs/project_list_item.tsx new file mode 100644 index 000000000000..e4aa1abd3693 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiTitle, + EuiText, + EuiFormFieldset, + EuiScreenReaderOnly, +} from '@elastic/eui'; +import classnames from 'classnames'; + +import { ProjectID, EnvironmentName, Project, environmentNames } from '../../../common/labs'; +import { EnvironmentSwitch } from './environment_switch'; + +import { LabsStrings } from '../../i18n'; +const { ListItem: strings } = LabsStrings.Components; + +import './project_list_item.scss'; + +export interface Props { + project: Project; + onStatusChange: (id: ProjectID, env: EnvironmentName, enabled: boolean) => void; +} + +export const ProjectListItem = ({ project, onStatusChange }: Props) => { + const { id, status, isActive, name, description, solutions } = project; + const { isEnabled, isOverride } = status; + + return ( + + + + + + +

    {name}

    +
    +
    + +
    + {solutions.map((solution) => ( + {solution} + ))} +
    +
    + + {description} + + + + {isActive ? strings.getEnabledStatusMessage() : strings.getDisabledStatusMessage()} + + +
    +
    + + + + {name} + + {strings.getOverrideLegend()} + + ), + }} + > + {environmentNames.map((env) => { + const envStatus = status[env]; + if (envStatus !== undefined) { + return ( + onStatusChange(id, env, checked)} + {...{ env, name }} + /> + ); + } + })} + + +
    +
    + ); +}; diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx index 4491be04b1a4..6c36cf8b8e3a 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx @@ -10,32 +10,15 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { - OnSaveProps, - SaveModalState, - SavedObjectSaveModal, -} from '../../../../plugins/saved_objects/public'; +import { OnSaveProps, SavedObjectSaveModal } from '../../../../plugins/saved_objects/public'; -import './saved_object_save_modal_dashboard.scss'; import { pluginServices } from '../services'; +import { SaveModalDashboardProps } from './types'; import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector'; -interface SaveModalDocumentInfo { - id?: string; - title: string; - description?: string; -} - -export interface SaveModalDashboardProps { - documentInfo: SaveModalDocumentInfo; - canSaveByReference: boolean; - objectType: string; - onClose: () => void; - onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; - tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); -} +import './saved_object_save_modal_dashboard.scss'; -export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { +function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { const { documentInfo, tagOptions, objectType, onClose, canSaveByReference } = props; const { id: documentId } = documentInfo; const initialCopyOnSave = !Boolean(documentId); @@ -136,3 +119,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { /> ); } + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default SavedObjectSaveModalDashboard; diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx index 78a1569c02ea..53aaecb070c7 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx @@ -22,7 +22,7 @@ import { EuiCheckbox, } from '@elastic/eui'; -import { DashboardPicker, DashboardPickerProps } from './dashboard_picker'; +import DashboardPicker, { DashboardPickerProps } from './dashboard_picker'; import './saved_object_save_modal_dashboard.scss'; diff --git a/src/plugins/presentation_util/public/components/types.ts b/src/plugins/presentation_util/public/components/types.ts new file mode 100644 index 000000000000..7c5c50982f49 --- /dev/null +++ b/src/plugins/presentation_util/public/components/types.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { OnSaveProps, SaveModalState } from '../../../../plugins/saved_objects/public'; + +interface SaveModalDocumentInfo { + id?: string; + title: string; + description?: string; +} + +export interface SaveModalDashboardProps { + documentInfo: SaveModalDocumentInfo; + canSaveByReference: boolean; + objectType: string; + onClose: () => void; + onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; + tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); +} diff --git a/src/legacy/server/config/index.js b/src/plugins/presentation_util/public/i18n/index.ts similarity index 91% rename from src/legacy/server/config/index.js rename to src/plugins/presentation_util/public/i18n/index.ts index 6fb77eb2a377..cf2f2c111ad5 100644 --- a/src/legacy/server/config/index.js +++ b/src/plugins/presentation_util/public/i18n/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { Config } from './config'; +export * from './labs'; diff --git a/src/plugins/presentation_util/public/i18n/labs.tsx b/src/plugins/presentation_util/public/i18n/labs.tsx new file mode 100644 index 000000000000..ddf6346bd68c --- /dev/null +++ b/src/plugins/presentation_util/public/i18n/labs.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const LabsStrings = { + Components: { + Switch: { + getKibanaSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.kibanaSwitchName', { + defaultMessage: 'Kibana', + }), + help: i18n.translate('presentationUtil.labs.components.kibanaSwitchHelp', { + defaultMessage: 'Sets the corresponding Advanced Setting for this lab project in Kibana', + }), + }), + getBrowserSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.browserSwitchName', { + defaultMessage: 'Browser', + }), + help: i18n.translate('presentationUtil.labs.components.browserSwitchHelp', { + defaultMessage: + 'Enables or disables the lab project for the browser; persists between browser instances', + }), + }), + getSessionSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.sessionSwitchName', { + defaultMessage: 'Session', + }), + help: i18n.translate('presentationUtil.labs.components.sessionSwitchHelp', { + defaultMessage: + 'Enables or disables the lab project for this tab; resets when the browser tab is closed', + }), + }), + }, + List: { + getNoProjectsMessage: () => + i18n.translate('presentationUtil.labs.components.noProjectsMessage', { + defaultMessage: 'No available lab projects', + }), + }, + ListItem: { + getOverrideLegend: () => + i18n.translate('presentationUtil.labs.components.overrideFlagsLabel', { + defaultMessage: 'Override flags', + }), + getEnabledStatusMessage: () => ( + Enabled, + }} + description="Displays the current status of a lab project" + /> + ), + getDisabledStatusMessage: () => ( + Disabled, + }} + description="Displays the current status of a lab project" + /> + ), + }, + Flyout: { + getTitleLabel: () => + i18n.translate('presentationUtil.labs.components.titleLabel', { + defaultMessage: 'Lab projects', + }), + getResetToDefaultLabel: () => + i18n.translate('presentationUtil.labs.components.resetToDefaultLabel', { + defaultMessage: 'Reset to defaults', + }), + getLabFlagsLabel: () => + i18n.translate('presentationUtil.labs.components.labFlagsLabel', { + defaultMessage: 'Lab flags', + }), + getRefreshLabel: () => + i18n.translate('presentationUtil.labs.components.calloutHelp', { + defaultMessage: 'Refresh to apply changes', + }), + }, + }, +}; diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index f13807032db3..1cbf4b5a4f33 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -8,14 +8,18 @@ import { PresentationUtilPlugin } from './plugin'; -export { - SavedObjectSaveModalDashboard, - SaveModalDashboardProps, -} from './components/saved_object_save_modal_dashboard'; +export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; +export { SaveModalDashboardProps } from './components/types'; +export { projectIDs, ProjectID, Project } from '../common/labs'; -export { DashboardPicker } from './components/dashboard_picker'; +export { + LazyLabsBeakerButton, + LazyLabsFlyout, + LazyDashboardPicker, + LazySavedObjectSaveModalDashboard, + withSuspense, +} from './components'; export function plugin() { return new PresentationUtilPlugin(); } -export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index 6f74198bb56a..00931c5730fe 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -36,9 +36,9 @@ export class PresentationUtilPlugin startPlugins: PresentationUtilPluginStartDeps ): PresentationUtilPluginStart { pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); - return { ContextProvider: pluginServices.getContextProvider(), + labsService: pluginServices.getServices().labs, }; } diff --git a/src/core/server/utils/from_root.ts b/src/plugins/presentation_util/public/services/capabilities.ts similarity index 67% rename from src/core/server/utils/from_root.ts rename to src/plugins/presentation_util/public/services/capabilities.ts index 377f4d0e29ca..58d56d1a4d81 100644 --- a/src/core/server/utils/from_root.ts +++ b/src/plugins/presentation_util/public/services/capabilities.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { resolve } from 'path'; -import { pkg } from './package_json'; - -export function fromRoot(...args: string[]) { - return resolve(pkg.__dirname, ...args); +export interface PresentationCapabilitiesService { + canAccessDashboards: () => boolean; + canCreateNewDashboards: () => boolean; + canSaveVisualizations: () => boolean; } diff --git a/src/plugins/presentation_util/public/services/create/index.ts b/src/plugins/presentation_util/public/services/create/index.ts index 66f718591332..163e25e26bab 100644 --- a/src/plugins/presentation_util/public/services/create/index.ts +++ b/src/plugins/presentation_util/public/services/create/index.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import { mapValues } from 'lodash'; - import { PluginServiceRegistry } from './registry'; export { PluginServiceRegistry } from './registry'; @@ -18,6 +16,8 @@ export { KibanaPluginServiceParams, } from './factory'; +type ServiceHooks = { [K in keyof Services]: { useService: () => Services[K] } }; + /** * `PluginServices` is a top-level class for specifying and accessing services within a plugin. * @@ -70,13 +70,27 @@ export class PluginServices { /** * Return a map of React Hooks that can be used in React components. */ - getHooks(): { [K in keyof Services]: { useService: () => Services[K] } } { + getHooks(): ServiceHooks { const registry = this.getRegistry(); const providers = registry.getServiceProviders(); - // @ts-expect-error Need to fix this; the type isn't fully understood when inferred. - return mapValues(providers, (provider) => ({ - useService: provider.getUseServiceHook(), - })); + const providerNames = Object.keys(providers) as Array; + + return providerNames.reduce((acc, providerName) => { + acc[providerName] = { useService: providers[providerName].getServiceReactHook() }; + return acc; + }, {} as ServiceHooks); + } + + getServices(): Services { + const registry = this.getRegistry(); + const providers = registry.getServiceProviders(); + + const providerNames = Object.keys(providers) as Array; + + return providerNames.reduce((acc, providerName) => { + acc[providerName] = providers[providerName].getService(); + return acc; + }, {} as Services); } } diff --git a/src/plugins/presentation_util/public/services/create/provider.tsx b/src/plugins/presentation_util/public/services/create/provider.tsx index fa16e291a656..06590bcfbb3d 100644 --- a/src/plugins/presentation_util/public/services/create/provider.tsx +++ b/src/plugins/presentation_util/public/services/create/provider.tsx @@ -41,9 +41,9 @@ export class PluginServiceProvider { } /** - * Private getter that will enforce proper setup throughout the class. + * Getter that will enforce proper setup throughout the class. */ - private getService() { + public getService() { if (!this.pluginService) { throw new Error('Service not started'); } @@ -62,7 +62,7 @@ export class PluginServiceProvider { /** * Returns a function for providing a Context hook for the service. */ - getUseServiceHook() { + getServiceReactHook() { return () => { const service = useContext(this.context); diff --git a/src/plugins/presentation_util/public/services/create/registry.tsx b/src/plugins/presentation_util/public/services/create/registry.tsx index 61ada16e241a..e8f85666bcac 100644 --- a/src/plugins/presentation_util/public/services/create/registry.tsx +++ b/src/plugins/presentation_util/public/services/create/registry.tsx @@ -7,7 +7,6 @@ */ import React from 'react'; -import { values } from 'lodash'; import { PluginServiceProvider, PluginServiceProviders } from './provider'; /** @@ -47,16 +46,17 @@ export class PluginServiceRegistry { * Returns a React Context Provider for use in consuming applications. */ getContextProvider() { + const values = Object.values(this.getServiceProviders()) as Array< + PluginServiceProvider + >; + // Collect and combine Context.Provider elements from each Service Provider into a single // Functional Component. const provider: React.FC = ({ children }) => ( <> - {values>(this.getServiceProviders()).reduceRight( - (acc, serviceProvider) => { - return {acc}; - }, - children - )} + {values.reduceRight((acc, serviceProvider) => { + return {acc}; + }, children)} ); @@ -69,9 +69,8 @@ export class PluginServiceRegistry { * @param params Parameters used to start the registry. */ start(params: StartParameters) { - values>(this.providers).map((serviceProvider) => - serviceProvider.start(params) - ); + const providerNames = Object.keys(this.providers) as Array; + providerNames.forEach((providerName) => this.providers[providerName].start(params)); this._isStarted = true; return this; } @@ -80,9 +79,8 @@ export class PluginServiceRegistry { * Stop the registry. */ stop() { - values>(this.providers).map((serviceProvider) => - serviceProvider.stop() - ); + const providerNames = Object.keys(this.providers) as Array; + providerNames.forEach((providerName) => this.providers[providerName].stop()); this._isStarted = false; return this; } diff --git a/src/plugins/presentation_util/public/services/dashboards.ts b/src/plugins/presentation_util/public/services/dashboards.ts new file mode 100644 index 000000000000..cbca79223063 --- /dev/null +++ b/src/plugins/presentation_util/public/services/dashboards.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SimpleSavedObject } from 'src/core/public'; +import { PartialDashboardAttributes } from './kibana/dashboards'; + +export interface PresentationDashboardsService { + findDashboards: ( + query: string, + fields: string[] + ) => Promise>>; + findDashboardsByTitle: ( + title: string + ) => Promise>>; +} diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts index 39dae92aa2ba..c01a95f64619 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -6,29 +6,14 @@ * Side Public License, v 1. */ -import { SimpleSavedObject } from 'src/core/public'; import { PluginServices } from './create'; -import { PartialDashboardAttributes } from './kibana/dashboards'; - -export interface PresentationDashboardsService { - findDashboards: ( - query: string, - fields: string[] - ) => Promise>>; - findDashboardsByTitle: ( - title: string - ) => Promise>>; -} - -export interface PresentationCapabilitiesService { - canAccessDashboards: () => boolean; - canCreateNewDashboards: () => boolean; - canSaveVisualizations: () => boolean; -} - +import { PresentationCapabilitiesService } from './capabilities'; +import { PresentationDashboardsService } from './dashboards'; +import { PresentationLabsService } from './labs'; export interface PresentationUtilServices { dashboards: PresentationDashboardsService; capabilities: PresentationCapabilitiesService; + labs: PresentationLabsService; } export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts index 6949fba00c65..d46af31b3066 100644 --- a/src/plugins/presentation_util/public/services/kibana/capabilities.ts +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -8,7 +8,7 @@ import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; export type CapabilitiesServiceFactory = KibanaPluginServiceFactory< PresentationCapabilitiesService, diff --git a/src/plugins/presentation_util/public/services/kibana/dashboards.ts b/src/plugins/presentation_util/public/services/kibana/dashboards.ts index 8735fe7fe266..59e3ada10a86 100644 --- a/src/plugins/presentation_util/public/services/kibana/dashboards.ts +++ b/src/plugins/presentation_util/public/services/kibana/dashboards.ts @@ -8,7 +8,7 @@ import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; -import { PresentationDashboardsService } from '..'; +import { PresentationDashboardsService } from '../dashboards'; export type DashboardsServiceFactory = KibanaPluginServiceFactory< PresentationDashboardsService, diff --git a/src/plugins/presentation_util/public/services/kibana/index.ts b/src/plugins/presentation_util/public/services/kibana/index.ts index 75388a71d14c..880f0f8b49c7 100644 --- a/src/plugins/presentation_util/public/services/kibana/index.ts +++ b/src/plugins/presentation_util/public/services/kibana/index.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { dashboardsServiceFactory } from './dashboards'; import { capabilitiesServiceFactory } from './capabilities'; +import { dashboardsServiceFactory } from './dashboards'; +import { labsServiceFactory } from './labs'; import { PluginServiceProviders, KibanaPluginServiceParams, @@ -17,15 +18,17 @@ import { import { PresentationUtilPluginStartDeps } from '../../types'; import { PresentationUtilServices } from '..'; -export { dashboardsServiceFactory } from './dashboards'; export { capabilitiesServiceFactory } from './capabilities'; +export { dashboardsServiceFactory } from './dashboards'; +export { labsServiceFactory } from './labs'; export const providers: PluginServiceProviders< PresentationUtilServices, KibanaPluginServiceParams > = { - dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), + dashboards: new PluginServiceProvider(dashboardsServiceFactory), }; export const registry = new PluginServiceRegistry< diff --git a/src/plugins/presentation_util/public/services/kibana/labs.ts b/src/plugins/presentation_util/public/services/kibana/labs.ts new file mode 100644 index 000000000000..d2c0735c76ee --- /dev/null +++ b/src/plugins/presentation_util/public/services/kibana/labs.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + environmentNames, + EnvironmentName, + projectIDs, + projects, + ProjectID, + Project, + getProjectIDs, +} from '../../../common'; +import { PresentationUtilPluginStartDeps } from '../../types'; +import { KibanaPluginServiceFactory } from '../create'; +import { + PresentationLabsService, + isEnabledByStorageValue, + setStorageStatus, + setUISettingsStatus, + applyProjectStatus, +} from '../labs'; + +export type LabsServiceFactory = KibanaPluginServiceFactory< + PresentationLabsService, + PresentationUtilPluginStartDeps +>; + +export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { + const { uiSettings } = coreStart; + const localStorage = window.localStorage; + const sessionStorage = window.sessionStorage; + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + + const status = { + session: isEnabledByStorageValue(project, 'session', sessionStorage.getItem(id)), + browser: isEnabledByStorageValue(project, 'browser', localStorage.getItem(id)), + kibana: isEnabledByStorageValue(project, 'kibana', uiSettings.get(id, project.isActive)), + }; + + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (name: ProjectID, env: EnvironmentName, status: boolean) => { + switch (env) { + case 'session': + setStorageStatus(sessionStorage, name, status); + break; + case 'browser': + setStorageStatus(localStorage, name, status); + break; + case 'kibana': + setUISettingsStatus(uiSettings, name, status); + break; + } + }; + + const reset = () => { + localStorage.clear(); + sessionStorage.clear(); + environmentNames.forEach((env) => + projectIDs.forEach((id) => setProjectStatus(id, env, projects[id].isActive)) + ); + }; + + return { + getProjectIDs, + getProjects, + getProject, + reset, + setProjectStatus, + }; +}; diff --git a/src/plugins/presentation_util/public/services/labs.ts b/src/plugins/presentation_util/public/services/labs.ts new file mode 100644 index 000000000000..72e9a232ea97 --- /dev/null +++ b/src/plugins/presentation_util/public/services/labs.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IUiSettingsClient } from 'kibana/public'; +import { + EnvironmentName, + projectIDs, + Project, + ProjectConfig, + ProjectID, + EnvironmentStatus, + environmentNames, + isProjectEnabledByStatus, +} from '../../common'; + +export interface PresentationLabsService { + getProjectIDs: () => typeof projectIDs; + getProject: (id: ProjectID) => Project; + getProjects: () => Record; + setProjectStatus: (id: ProjectID, env: EnvironmentName, status: boolean) => void; + reset: () => void; +} + +export const isEnabledByStorageValue = ( + project: ProjectConfig, + environment: EnvironmentName, + value: string | boolean | null +): boolean => { + const defaultValue = project.isActive; + + if (!project.environments.includes(environment)) { + return defaultValue; + } + + if (value === true || value === false) { + return value; + } + + if (value === 'enabled') { + return true; + } + + if (value === 'disabled') { + return false; + } + + return defaultValue; +}; + +export const setStorageStatus = (storage: Storage, id: ProjectID, enabled: boolean) => + storage.setItem(id, enabled ? 'enabled' : 'disabled'); + +export const applyProjectStatus = (project: ProjectConfig, status: EnvironmentStatus): Project => { + const { isActive, environments } = project; + + environmentNames.forEach((name) => { + if (!environments.includes(name)) { + delete status[name]; + } + }); + + const isEnabled = isProjectEnabledByStatus(isActive, status); + const isOverride = isEnabled !== isActive; + + return { + ...project, + status: { + ...status, + defaultValue: isActive, + isEnabled, + isOverride, + }, + }; +}; + +export const setUISettingsStatus = (client: IUiSettingsClient, id: ProjectID, enabled: boolean) => + client.set(id, enabled); diff --git a/src/plugins/presentation_util/public/services/storybook/capabilities.ts b/src/plugins/presentation_util/public/services/storybook/capabilities.ts index 16fbe3baf488..60285f00993a 100644 --- a/src/plugins/presentation_util/public/services/storybook/capabilities.ts +++ b/src/plugins/presentation_util/public/services/storybook/capabilities.ts @@ -8,7 +8,7 @@ import { PluginServiceFactory } from '../create'; import { StorybookParams } from '.'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; type CapabilitiesServiceFactory = PluginServiceFactory< PresentationCapabilitiesService, diff --git a/src/plugins/presentation_util/public/services/storybook/index.ts b/src/plugins/presentation_util/public/services/storybook/index.ts index dd7de5426406..37669d52c009 100644 --- a/src/plugins/presentation_util/public/services/storybook/index.ts +++ b/src/plugins/presentation_util/public/services/storybook/index.ts @@ -8,6 +8,7 @@ import { PluginServices, PluginServiceProviders, PluginServiceProvider } from '../create'; import { dashboardsServiceFactory } from '../stub/dashboards'; +import { labsServiceFactory } from './labs'; import { capabilitiesServiceFactory } from './capabilities'; import { PresentationUtilServices } from '..'; @@ -22,8 +23,9 @@ export interface StorybookParams { } export const providers: PluginServiceProviders = { - dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + dashboards: new PluginServiceProvider(dashboardsServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), }; export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/storybook/labs.ts b/src/plugins/presentation_util/public/services/storybook/labs.ts new file mode 100644 index 000000000000..8878e218f19e --- /dev/null +++ b/src/plugins/presentation_util/public/services/storybook/labs.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EnvironmentName, projectIDs, Project } from '../../../common'; +import { PluginServiceFactory } from '../create'; +import { projects, ProjectID, getProjectIDs } from '../../../common'; +import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from '../labs'; + +export type LabsServiceFactory = PluginServiceFactory; + +export const labsServiceFactory: LabsServiceFactory = () => { + const storage = window.sessionStorage; + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + const { isActive } = project; + const status = { + session: isEnabledByStorageValue(project, 'session', sessionStorage.getItem(id)), + browser: isEnabledByStorageValue(project, 'browser', localStorage.getItem(id)), + kibana: isActive, + }; + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (name: ProjectID, env: EnvironmentName, enabled: boolean) => { + if (env === 'session') { + storage.setItem(name, enabled ? 'enabled' : 'disabled'); + } + }; + + const reset = () => { + storage.clear(); + }; + + return { + getProjectIDs, + getProjects, + getProject, + reset, + setProjectStatus, + }; +}; diff --git a/src/plugins/presentation_util/public/services/stub/capabilities.ts b/src/plugins/presentation_util/public/services/stub/capabilities.ts index 4154fa65a0cd..80b913c4f085 100644 --- a/src/plugins/presentation_util/public/services/stub/capabilities.ts +++ b/src/plugins/presentation_util/public/services/stub/capabilities.ts @@ -7,7 +7,7 @@ */ import { PluginServiceFactory } from '../create'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; type CapabilitiesServiceFactory = PluginServiceFactory; diff --git a/src/plugins/presentation_util/public/services/stub/dashboards.ts b/src/plugins/presentation_util/public/services/stub/dashboards.ts index 280ae9582b81..047176836896 100644 --- a/src/plugins/presentation_util/public/services/stub/dashboards.ts +++ b/src/plugins/presentation_util/public/services/stub/dashboards.ts @@ -7,7 +7,7 @@ */ import { PluginServiceFactory } from '../create'; -import { PresentationDashboardsService } from '..'; +import { PresentationDashboardsService } from '../dashboards'; // TODO (clint): Create set of dashboards to stub and return. diff --git a/src/plugins/presentation_util/public/services/stub/index.ts b/src/plugins/presentation_util/public/services/stub/index.ts index d1a8147f8fb8..6bf32bba00a3 100644 --- a/src/plugins/presentation_util/public/services/stub/index.ts +++ b/src/plugins/presentation_util/public/services/stub/index.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { dashboardsServiceFactory } from './dashboards'; import { capabilitiesServiceFactory } from './capabilities'; +import { dashboardsServiceFactory } from './dashboards'; +import { labsServiceFactory } from './labs'; import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create'; import { PresentationUtilServices } from '..'; @@ -17,6 +18,7 @@ export { capabilitiesServiceFactory } from './capabilities'; export const providers: PluginServiceProviders = { dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), }; export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/presentation_util/public/services/stub/labs.ts b/src/plugins/presentation_util/public/services/stub/labs.ts new file mode 100644 index 000000000000..c83bb68b5d07 --- /dev/null +++ b/src/plugins/presentation_util/public/services/stub/labs.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + projects, + projectIDs, + ProjectID, + EnvironmentName, + getProjectIDs, + Project, +} from '../../../common'; +import { PluginServiceFactory } from '../create'; +import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from '../labs'; + +export type LabsServiceFactory = PluginServiceFactory; + +export const labsServiceFactory: LabsServiceFactory = () => { + const reset = () => + projectIDs.reduce((acc, id) => { + const project = getProject(id); + const defaultValue = project.isActive; + + acc[id] = { + defaultValue, + session: null, + browser: null, + kibana: defaultValue, + }; + return acc; + }, {} as { [id in ProjectID]: { defaultValue: boolean; session: boolean | null; browser: boolean | null; kibana: boolean } }); + + let statuses = reset(); + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + const value = statuses[id]; + const status = { + session: isEnabledByStorageValue(project, 'session', value.session), + browser: isEnabledByStorageValue(project, 'browser', value.browser), + kibana: isEnabledByStorageValue(project, 'kibana', value.kibana), + }; + + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (id: ProjectID, env: EnvironmentName, value: boolean) => { + statuses[id] = { ...statuses[id], [env]: value }; + }; + + return { + getProjectIDs, + getProject, + getProjects, + setProjectStatus, + reset: () => { + statuses = reset(); + }, + }; +}; diff --git a/src/plugins/presentation_util/public/types.ts b/src/plugins/presentation_util/public/types.ts index f1bd6c1b747e..05779ffb206c 100644 --- a/src/plugins/presentation_util/public/types.ts +++ b/src/plugins/presentation_util/public/types.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ +import { PresentationLabsService } from './services/labs'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PresentationUtilPluginSetup {} export interface PresentationUtilPluginStart { ContextProvider: React.FC; + labsService: PresentationLabsService; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/core/server/legacy/config/index.ts b/src/plugins/presentation_util/server/index.ts similarity index 76% rename from src/core/server/legacy/config/index.ts rename to src/plugins/presentation_util/server/index.ts index b674b1386b78..de7e8de40544 100644 --- a/src/core/server/legacy/config/index.ts +++ b/src/plugins/presentation_util/server/index.ts @@ -6,4 +6,6 @@ * Side Public License, v 1. */ -export { ensureValidConfiguration } from './ensure_valid_configuration'; +import { PresentationUtilPlugin } from './plugin'; + +export const plugin = () => new PresentationUtilPlugin(); diff --git a/src/core/server/utils/package_json.ts b/src/plugins/presentation_util/server/plugin.ts similarity index 51% rename from src/core/server/utils/package_json.ts rename to src/plugins/presentation_util/server/plugin.ts index 57ca781d7d78..eb5537392062 100644 --- a/src/core/server/utils/package_json.ts +++ b/src/plugins/presentation_util/server/plugin.ts @@ -6,10 +6,18 @@ * Side Public License, v 1. */ -import { dirname } from 'path'; +import { CoreSetup, Plugin } from 'kibana/server'; +import { getUISettings } from './ui_settings'; -export const pkg = { - __filename: require.resolve('../../../../package.json'), - __dirname: dirname(require.resolve('../../../../package.json')), - ...require('../../../../package.json'), -}; +export class PresentationUtilPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register(getUISettings()); + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/presentation_util/server/ui_settings.ts b/src/plugins/presentation_util/server/ui_settings.ts new file mode 100644 index 000000000000..450354832c3a --- /dev/null +++ b/src/plugins/presentation_util/server/ui_settings.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '../../../../src/core/types'; +import { projects, projectIDs, ProjectID } from '../common'; + +export const SETTING_CATEGORY = 'Presentation Labs'; + +const labsProjectSettings: Record> = projectIDs.reduce( + (acc, id) => { + const project = projects[id]; + const { name, description, isActive: value } = project; + acc[id] = { + name, + value, + type: 'boolean', + description, + schema: schema.boolean(), + requiresPageReload: true, + category: [SETTING_CATEGORY], + }; + return acc; + }, + {} as { + [id in ProjectID]: UiSettingsParams; + } +); + +/** + * uiSettings definitions for Presentation Util. + */ +export const getUISettings = (): Record> => ({ + ...labsProjectSettings, +}); diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index cb39c5fb36f5..63d136cf9445 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", @@ -7,9 +7,19 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*", "storybook/**/*", "../../../typings/**/*"], + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "storybook/**/*", + "../../../typings/**/*" + ], "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../saved_objects/tsconfig.json" }, + { + "path": "../../core/tsconfig.json" + }, + { + "path": "../saved_objects/tsconfig.json" + }, ] } diff --git a/src/plugins/region_map/tsconfig.json b/src/plugins/region_map/tsconfig.json index 385c31e6bd2d..899611d02746 100644 --- a/src/plugins/region_map/tsconfig.json +++ b/src/plugins/region_map/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/saved_objects/tsconfig.json b/src/plugins/saved_objects/tsconfig.json index 46b3f9a5afcb..d9045b91b9df 100644 --- a/src/plugins/saved_objects/tsconfig.json +++ b/src/plugins/saved_objects/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/saved_objects_management/tsconfig.json b/src/plugins/saved_objects_management/tsconfig.json index cb74b0617922..99849dea3861 100644 --- a/src/plugins/saved_objects_management/tsconfig.json +++ b/src/plugins/saved_objects_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/saved_objects_tagging_oss/tsconfig.json b/src/plugins/saved_objects_tagging_oss/tsconfig.json index ae566d962689..b0059c71424b 100644 --- a/src/plugins/saved_objects_tagging_oss/tsconfig.json +++ b/src/plugins/saved_objects_tagging_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/security_oss/tsconfig.json b/src/plugins/security_oss/tsconfig.json index 156e4ffee9d7..530e01a034b0 100644 --- a/src/plugins/security_oss/tsconfig.json +++ b/src/plugins/security_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/share/tsconfig.json b/src/plugins/share/tsconfig.json index 62c0b3739f4b..985066915f1d 100644 --- a/src/plugins/share/tsconfig.json +++ b/src/plugins/share/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/spaces_oss/tsconfig.json b/src/plugins/spaces_oss/tsconfig.json index 96584842ec32..0cc82d7e5d12 100644 --- a/src/plugins/spaces_oss/tsconfig.json +++ b/src/plugins/spaces_oss/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 05ac1eb84089..d8bcf150ac16 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8137,6 +8137,12 @@ "_meta": { "description": "Non-default value of setting." } + }, + "labs:presentation:unifiedToolbar": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } } } }, diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index 40370082f99e..bdced01d9eb6 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json index 8bbc440fb1a5..1bba81769f0d 100644 --- a/src/plugins/telemetry_collection_manager/tsconfig.json +++ b/src/plugins/telemetry_collection_manager/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index c6a21733b2d6..48e40814b857 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/tile_map/tsconfig.json b/src/plugins/tile_map/tsconfig.json index 385c31e6bd2d..899611d02746 100644 --- a/src/plugins/tile_map/tsconfig.json +++ b/src/plugins/tile_map/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/timelion/tsconfig.json b/src/plugins/timelion/tsconfig.json index bb8a48339d44..5b96d69a878e 100644 --- a/src/plugins/timelion/tsconfig.json +++ b/src/plugins/timelion/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/ui_actions/tsconfig.json b/src/plugins/ui_actions/tsconfig.json index 89b66d18705c..a871d7215cdc 100644 --- a/src/plugins/ui_actions/tsconfig.json +++ b/src/plugins/ui_actions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/url_forwarding/tsconfig.json b/src/plugins/url_forwarding/tsconfig.json index f1916e4ce595..8e867a6bad14 100644 --- a/src/plugins/url_forwarding/tsconfig.json +++ b/src/plugins/url_forwarding/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index b4a0721ef367..96b2c4d37e17 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_default_editor/tsconfig.json b/src/plugins/vis_default_editor/tsconfig.json index 54a84e08224a..27bb775c2d0e 100644 --- a/src/plugins/vis_default_editor/tsconfig.json +++ b/src/plugins/vis_default_editor/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_markdown/tsconfig.json b/src/plugins/vis_type_markdown/tsconfig.json index f940c295e7ce..d5ab89b98081 100644 --- a/src/plugins/vis_type_markdown/tsconfig.json +++ b/src/plugins/vis_type_markdown/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_metric/tsconfig.json b/src/plugins/vis_type_metric/tsconfig.json index 8cee918a3dc8..7441848d5a43 100644 --- a/src/plugins/vis_type_metric/tsconfig.json +++ b/src/plugins/vis_type_metric/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_table/tsconfig.json b/src/plugins/vis_type_table/tsconfig.json index 4f2e80575497..ccff3c349cf2 100644 --- a/src/plugins/vis_type_table/tsconfig.json +++ b/src/plugins/vis_type_table/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_tagcloud/tsconfig.json b/src/plugins/vis_type_tagcloud/tsconfig.json index f7f3688183a4..18bbad225746 100644 --- a/src/plugins/vis_type_tagcloud/tsconfig.json +++ b/src/plugins/vis_type_tagcloud/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_timelion/tsconfig.json b/src/plugins/vis_type_timelion/tsconfig.json index d29fb25b1531..77f97de28366 100644 --- a/src/plugins/vis_type_timelion/tsconfig.json +++ b/src/plugins/vis_type_timelion/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx index 25965d796e65..d02565717b24 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx @@ -48,9 +48,9 @@ export function Agg(props: AggProps) { ...props.style, }; - const indexPattern = - (props.series.override_index_pattern && props.series.series_index_pattern) || - props.panel.index_pattern; + const indexPattern = props.series.override_index_pattern + ? props.series.series_index_pattern + : props.panel.index_pattern; return (
    diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js index 19d2b88c8d12..7f93567980b2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js @@ -51,8 +51,9 @@ export const FilterRatioAgg = (props) => { (query) => handleChange({ denominator: query }), [handleChange] ); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const defaults = { numerator: getDataStart().query.queryString.getDefaultQuery(), diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js index 505a2ff4f3c7..77b2e2f02030 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js @@ -39,8 +39,9 @@ export function PercentileAgg(props) { const handleSelectChange = createSelectHandler(handleChange); const handleNumberChange = createNumberHandler(handleChange); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; useEffect(() => { if (!checkModel(model)) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js index a80194f72b7b..4b1528ca2708 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js @@ -65,9 +65,9 @@ export const PositiveRateAgg = (props) => { const handleSelectChange = createSelectHandler(handleChange); const htmlId = htmlIdGenerator(); - const indexPattern = - (props.series.override_index_pattern && props.series.series_index_pattern) || - props.panel.index_pattern; + const indexPattern = props.series.override_index_pattern + ? props.series.series_index_pattern + : props.panel.index_pattern; const selectedUnitOptions = UNIT_OPTIONS.filter((o) => o.value === model.unit); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js index 4a4114f70f06..74b441f44630 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js @@ -31,8 +31,9 @@ export function StandardAgg(props) { const handleSelectChange = createSelectHandler(handleChange); const restrictFields = getSupportedFieldsByMetricType(model.type); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const htmlId = htmlIdGenerator(); return ( diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js index c28cb294c330..749a97fa79f2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js @@ -72,8 +72,9 @@ const StandardDeviationAggUi = (props) => { const handleSelectChange = createSelectHandler(handleChange); const handleTextChange = createTextHandler(handleChange); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const htmlId = htmlIdGenerator(); const selectedModeOption = modeOptions.find((option) => { return model.mode === option.value; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js index 9bea32b7cbd5..92e754c1dcda 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js @@ -100,8 +100,9 @@ const TopHitAggUi = (props) => { order: 'desc', }; const model = { ...defaults, ...props.model }; - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const aggWithOptionsRestrictFields = [ PANEL_TYPES.TABLE, diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config.js b/src/plugins/vis_type_timeseries/public/application/components/series_config.js index 3185503acb56..8f3893feb89b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/series_config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/series_config.js @@ -33,10 +33,9 @@ export const SeriesConfig = (props) => { const handleSelectChange = createSelectHandler(props.onChange); const handleTextChange = createTextHandler(props.onChange); const htmlId = htmlIdGenerator(); - const seriesIndexPattern = - props.model.override_index_pattern && props.model.series_index_pattern - ? props.model.series_index_pattern - : props.indexPatternForQuery; + const seriesIndexPattern = props.model.override_index_pattern + ? props.model.series_index_pattern + : props.indexPatternForQuery; return (
    diff --git a/src/plugins/vis_type_timeseries/public/application/components/split.js b/src/plugins/vis_type_timeseries/public/application/components/split.js index 63aa717174a0..4990800acf6d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/split.js +++ b/src/plugins/vis_type_timeseries/public/application/components/split.js @@ -63,8 +63,9 @@ export class Split extends Component { render() { const { model, panel, uiRestrictions, seriesQuantity } = this.props; - const indexPattern = - (model.override_index_pattern && model.series_index_pattern) || panel.index_pattern; + const indexPattern = model.override_index_pattern + ? model.series_index_pattern + : panel.index_pattern; const splitMode = get(this.props, 'model.split_mode', SPLIT_MODES.EVERYTHING); const Component = this.getComponent(splitMode, uiRestrictions); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss index 198f0f42d503..8689dbb4e187 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss @@ -2,10 +2,14 @@ display: flex; flex-direction: column; flex: 1 1 100%; - overflow: auto; + position: relative; .tvbVisTimeSeries { - overflow: hidden; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; } .tvbVisTimeSeriesDark { .echReactiveChart_unavailable { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js index 22bf2fa4ca70..1c3a0411998b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js @@ -327,10 +327,9 @@ export const TimeseriesConfig = injectI18n(function (props) { const disableSeparateYaxis = model.separate_axis ? false : true; - const seriesIndexPattern = - props.model.override_index_pattern && props.model.series_index_pattern - ? props.model.series_index_pattern - : props.indexPatternForQuery; + const seriesIndexPattern = props.model.override_index_pattern + ? props.model.series_index_pattern + : props.indexPatternForQuery; const initialPalette = { ...model.palette, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index ae3fa4d9dcca..e3a3902ce1ba 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -225,21 +225,23 @@ class TimeseriesVisualization extends Component { return (
    - +
    + +
    ); } diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index a90faea50f22..f9a52a9450dc 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -16,7 +16,7 @@ import { Chart, Position, Settings, - AnnotationDomainTypes, + AnnotationDomainType, LineAnnotation, TooltipType, StackMode, @@ -86,7 +86,7 @@ export const TimeSeries = ({ const hasBarChart = series.some(({ bars }) => bars?.show); // apply legend style change if bgColor is configured - const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor)); + const classes = classNames(getChartClasses(backgroundColor)); // If the color isn't configured by the user, use the color mapping service // to assign a color from the Kibana palette. Colors will be shared across the @@ -149,6 +149,7 @@ export const TimeSeries = ({ tooltip={{ snap: true, type: tooltipMode === 'show_focused' ? TooltipType.Follow : TooltipType.VerticalCursor, + boundary: document.getElementById('app-fixed-viewport') ?? undefined, headerFormatter: tooltipFormatter, }} externalPointerEvents={{ tooltip: { visible: false } }} @@ -162,7 +163,7 @@ export const TimeSeries = ({ } hideLinesTooltips={true} diff --git a/src/plugins/vis_type_timeseries/tsconfig.json b/src/plugins/vis_type_timeseries/tsconfig.json index edc2d25b867d..7b2dd4b608c1 100644 --- a/src/plugins/vis_type_timeseries/tsconfig.json +++ b/src/plugins/vis_type_timeseries/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index f375a2483e24..4091dafcbe35 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_vislib/tsconfig.json b/src/plugins/vis_type_vislib/tsconfig.json index 845628a6b86f..74bc1440d9db 100644 --- a/src/plugins/vis_type_vislib/tsconfig.json +++ b/src/plugins/vis_type_vislib/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx index 59bed0060a6a..8922f512522a 100644 --- a/src/plugins/vis_type_xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx @@ -148,13 +148,15 @@ export const XYSettings: FC = ({ : headerValueFormatter && (tooltip.detailedTooltip ? undefined : ({ value }: any) => headerValueFormatter(value)); + const boundary = document.getElementById('app-fixed-viewport') ?? undefined; const tooltipProps: TooltipProps = tooltip.detailedTooltip ? { ...tooltip, + boundary, customTooltip: tooltip.detailedTooltip(headerFormatter), headerFormatter: undefined, } - : { ...tooltip, headerFormatter }; + : { ...tooltip, boundary, headerFormatter }; return ( = ({ { if (!anonymousUserCapabilities.visualize) return false; @@ -420,40 +425,47 @@ export const getTopNavConfig = ( const useByRefFlow = !!originatingApp || !dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; - const saveModal = useByRefFlow ? ( - {}} - originatingApp={originatingApp} - returnToOriginSwitchLabel={ - originatingApp && embeddableId - ? i18n.translate('visualize.topNavMenu.updatePanel', { - defaultMessage: 'Update panel on {originatingAppName}', - values: { - originatingAppName: stateTransfer.getAppNameFromId(originatingApp), - }, - }) - : undefined - } - /> - ) : ( - {}} - /> - ); + let saveModal; + + if (useByRefFlow) { + saveModal = ( + {}} + originatingApp={originatingApp} + returnToOriginSwitchLabel={ + originatingApp && embeddableId + ? i18n.translate('visualize.topNavMenu.updatePanel', { + defaultMessage: 'Update panel on {originatingAppName}', + values: { + originatingAppName: stateTransfer.getAppNameFromId(originatingApp), + }, + }) + : undefined + } + /> + ); + } else { + saveModal = ( + {}} + /> + ); + } + showSaveModal( saveModal, I18nContext, diff --git a/src/plugins/visualize/tsconfig.json b/src/plugins/visualize/tsconfig.json index 046202d82d1a..bc0891f39174 100644 --- a/src/plugins/visualize/tsconfig.json +++ b/src/plugins/visualize/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/test/functional/apps/dashboard/dashboard_filtering.ts b/test/functional/apps/dashboard/dashboard_filtering.ts index e995bc4e52c4..86c57efec818 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.ts +++ b/test/functional/apps/dashboard/dashboard_filtering.ts @@ -28,7 +28,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); - describe('dashboard filtering', function () { + // Failing: See https://github.com/elastic/kibana/issues/92522 + describe.skip('dashboard filtering', function () { this.tags('includeFirefox'); const populateDashboard = async () => { diff --git a/test/functional/apps/discover/_huge_fields.ts b/test/functional/apps/discover/_huge_fields.ts index 8cb39feb2e6b..b3e63e482e73 100644 --- a/test/functional/apps/discover/_huge_fields.ts +++ b/test/functional/apps/discover/_huge_fields.ts @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - describe('test large number of fields in sidebar', function () { + // FLAKY: https://github.com/elastic/kibana/issues/96113 + describe.skip('test large number of fields in sidebar', function () { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); await esArchiver.loadIfNeeded('large_fields'); diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 9726b097c8f6..1d65b9a68bd4 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -26,7 +26,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const testSubjects = getService('testSubjects'); - describe('saved queries saved objects', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/89477 + describe.skip('saved queries saved objects', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.js index e3ff1819aed1..e2227d4240d4 100644 --- a/test/functional/apps/management/_runtime_fields.js +++ b/test/functional/apps/management/_runtime_fields.js @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings']); const testSubjects = getService('testSubjects'); - describe('runtime fields', function () { + // Failing: See https://github.com/elastic/kibana/issues/95376 + describe.skip('runtime fields', function () { this.tags(['skipFirefox']); before(async function () { diff --git a/test/functional/apps/visualize/_tile_map.ts b/test/functional/apps/visualize/_tile_map.ts index 668aec6ac578..3af467affa1f 100644 --- a/test/functional/apps/visualize/_tile_map.ts +++ b/test/functional/apps/visualize/_tile_map.ts @@ -15,7 +15,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); - const testSubjects = getService('testSubjects'); const browser = getService('browser'); const PageObjects = getPageObjects([ 'common', @@ -221,63 +220,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); - - describe('zoom warning behavior', function describeIndexTests() { - // Zoom warning is only applicable to OSS - this.tags(['skipCloud', 'skipFirefox']); - - const waitForLoading = false; - let zoomWarningEnabled; - let last = false; - const toastDefaultLife = 6000; - - before(async function () { - await browser.setWindowSize(1280, 1000); - - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - log.debug('clickTileMap'); - await PageObjects.visualize.clickTileMap(); - await PageObjects.visualize.clickNewSearch(); - - zoomWarningEnabled = await testSubjects.exists('zoomWarningEnabled'); - log.debug(`Zoom warning enabled: ${zoomWarningEnabled}`); - - const zoomLevel = 9; - for (let i = 0; i < zoomLevel; i++) { - await PageObjects.tileMap.clickMapZoomIn(); - } - }); - - beforeEach(async function () { - await PageObjects.tileMap.clickMapZoomIn(waitForLoading); - }); - - afterEach(async function () { - if (!last) { - await PageObjects.common.sleep(toastDefaultLife); - await PageObjects.tileMap.clickMapZoomOut(waitForLoading); - } - }); - - it('should show warning at zoom 10', async () => { - await testSubjects.existOrFail('maxZoomWarning'); - }); - - it('should continue providing zoom warning if left alone', async () => { - await testSubjects.existOrFail('maxZoomWarning'); - }); - - it('should suppress zoom warning if suppress warnings button clicked', async () => { - last = true; - await PageObjects.visChart.waitForVisualization(); - await testSubjects.click('suppressZoomWarnings'); - await PageObjects.tileMap.clickMapZoomOut(waitForLoading); - await testSubjects.waitForDeleted('suppressZoomWarnings'); - await PageObjects.tileMap.clickMapZoomIn(waitForLoading); - - await testSubjects.missingOrFail('maxZoomWarning'); - }); - }); }); } diff --git a/test/visual_regression/services/visual_testing/visual_testing.ts b/test/visual_regression/services/visual_testing/visual_testing.ts index dab12de2cef6..d0a714d6759b 100644 --- a/test/visual_regression/services/visual_testing/visual_testing.ts +++ b/test/visual_regression/services/visual_testing/visual_testing.ts @@ -9,7 +9,7 @@ import { postSnapshot } from '@percy/agent/dist/utils/sdk-utils'; import testSubjSelector from '@kbn/test-subj-selector'; import { Test } from '@kbn/test/types/ftr'; -import { pkg } from '../../../../src/core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import { FtrProviderContext } from '../../ftr_provider_context'; // @ts-ignore internal js that is passed to the browser as is @@ -45,6 +45,7 @@ export async function VisualTestingProvider({ getService }: FtrProviderContext) }); const statsCache = new WeakMap(); + function getStats(test: Test) { if (!statsCache.has(test)) { statsCache.set(test, { diff --git a/tsconfig.base.json b/tsconfig.base.json index c28ed0d8c875..da4de5ef3712 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -56,6 +56,5 @@ "jest-styled-components", "@testing-library/jest-dom" ] - }, - "include": [] + } } diff --git a/tsconfig.json b/tsconfig.json index 03597114333c..40763ede1bbd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,78 +19,6 @@ "x-pack/plugins/cases/**/*", "x-pack/plugins/lists/**/*", "x-pack/plugins/security_solution/**/*", - - // tests - "src/**/*.test.ts", - "src/**/*.test.tsx", - "src/**/integration_tests/*", - "src/**/tests/*", - // mocks - "src/**/__mocks__/*", - "src/**/mock/*", - "src/**/mocks/*", - "src/**/*/mock.ts", - "src/**/*/mocks.ts", - "src/**/*/mocks.tsx", - "src/**/*.mock.ts", - "src/**/*.mock.tsx", - "src/**/*.mocks.ts", - "src/**/*.mocks.tsx", - - // test helpers - "src/**/test_helpers/*", - "src/**/test_utils/*", - "src/**/*/test_utils.ts", - "src/**/*/test_helpers.ts", - "src/**/*/test_helper.tsx", - - // stubs - "src/**/*/stubs.ts", - "src/**/*.stub.ts", - "src/**/*.stories.tsx", - "src/**/*/_mock_handler_arguments.ts", - - // tests - "x-pack/plugins/**/*.test.ts", - "x-pack/plugins/**/*.test.tsx", - "x-pack/plugins/**/test/**/*", - "x-pack/plugins/**/tests/*", - "x-pack/plugins/**/integration_tests/*", - "x-pack/plugins/**/tests_client_integration/*", - "x-pack/plugins/**/__fixtures__/*", - "x-pack/plugins/**/__stories__/*", - "x-pack/plugins/**/__jest__/**/*", - - // mocks - "x-pack/plugins/**/__mocks__/*", - "x-pack/plugins/**/mock/*", - "x-pack/plugins/**/mocks/*", - "x-pack/plugins/**/*/mock.ts", - "x-pack/plugins/**/*/mocks.ts", - "x-pack/plugins/**/*/mocks.tsx", - "x-pack/plugins/**/*.mock.ts", - "x-pack/plugins/**/*.mock.tsx", - "x-pack/plugins/**/*.mocks.ts", - "x-pack/plugins/**/*.mocks.tsx", - - // test helpers - "x-pack/plugins/**/test_helpers/*", - "x-pack/plugins/**/test_utils/*", - "x-pack/plugins/**/*/test_utils.ts", - "x-pack/plugins/**/*/test_helper.tsx", - "x-pack/plugins/**/*/test_helpers.ts", - "x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx", - "x-pack/plugins/uptime/server/lib/requests/helper.ts", - "x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx", - "x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx", - "x-pack/plugins/apm/server/utils/test_helpers.tsx", - "x-pack/plugins/apm/public/utils/testHelpers.tsx", - - // stubs - "x-pack/plugins/**/*/stubs.ts", - "x-pack/plugins/**/*.stub.ts", - "x-pack/plugins/**/*.stories.tsx", - "x-pack/plugins/**/*/_mock_handler_arguments.ts" ], "exclude": [ "x-pack/plugins/security_solution/cypress/**/*" @@ -180,7 +108,6 @@ { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, diff --git a/tsconfig.project.json b/tsconfig.project.json deleted file mode 100644 index 174c3fdf0fd5..000000000000 --- a/tsconfig.project.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "composite": true, - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "exclude": [ - // tests - "**/*.test.ts", - "**/*.test.tsx", - "**/integration_tests/*", - "**/test/**/*", - "**/test/*", - "**/tests/*", - "**/tests_client_integration/*", - "**/__fixtures__/*", - "**/__stories__/*", - "**/__jest__/**", - - // mocks - "**/__mocks__/*", - "**/mock/*", - "**/mocks/*", - "**/*/mock.ts", - "**/*/mocks.ts", - "**/*/mocks.tsx", - "**/*.mock.ts", - "**/*.mock.tsx", - "**/*.mocks.ts", - "**/*.mocks.tsx", - - // test helpers - "**/test_helpers/*", - "**/test_utils/*", - "**/*/test_utils.ts", - "**/*/test_helper.tsx", - "**/*/test_helpers.ts", - // x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx - "**/*/test_data.tsx", - "**/*/shared_columns_tests.tsx", - // x-pack/plugins/uptime/server/lib/requests/helper.ts - "**/*/requests/helper.ts", - // x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx - "**/*/rtl_helpers.tsx", - // x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx - "**/*/enzyme_helpers.tsx", - // x-pack/plugins/apm/server/utils/test_helpers.tsx - "**/*/test_helpers.tsx", - // x-pack/plugins/apm/public/utils/testHelpers.tsx - "**/*/testHelpers.tsx", - - // stubs - "**/*/stubs.ts", - "**/*.stub.ts", - "**/*.stories.tsx", - "**/*/_mock_handler_arguments.ts" - ], - "include": [], - "types": [ - "node", - "flot" - ] -} diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 2d9ddc1b9e56..f13455a14b4d 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -85,7 +85,6 @@ { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index 10ebd0923523..d5c1105c99ad 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index 401068874690..86ab00faeb5a 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/apm/e2e/tsconfig.json b/x-pack/plugins/apm/e2e/tsconfig.json index 0c13dd717991..c4587349c7ad 100644 --- a/x-pack/plugins/apm/e2e/tsconfig.json +++ b/x-pack/plugins/apm/e2e/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "exclude": ["tmp"], "include": ["./**/*"], "compilerOptions": { diff --git a/x-pack/plugins/apm/ftr_e2e/tsconfig.json b/x-pack/plugins/apm/ftr_e2e/tsconfig.json index f699943a254f..168801f78260 100644 --- a/x-pack/plugins/apm/ftr_e2e/tsconfig.json +++ b/x-pack/plugins/apm/ftr_e2e/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "exclude": [ "tmp" ], @@ -12,4 +12,4 @@ "node" ] } -} +} \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx index 619b4e3f3782..8a54c76df0f6 100644 --- a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx @@ -6,7 +6,7 @@ */ import { - AnnotationDomainTypes, + AnnotationDomainType, Axis, BarSeries, Chart, @@ -74,7 +74,7 @@ export function ChartPreview({ ({ dataValue: annotation['@timestamp'], header: asAbsoluteDateTime(annotation['@timestamp']), diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx index 23016cc5dd8e..436eca478150 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx @@ -6,7 +6,7 @@ */ import { - AnnotationDomainTypes, + AnnotationDomainType, AreaSeries, Axis, Chart, @@ -102,7 +102,7 @@ export function TransactionBreakdownChartContents({ {showAnnotations && ( ({ dataValue: annotation['@timestamp'], header: asAbsoluteDateTime(annotation['@timestamp']), diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts index 1821e92ee5a7..29fabc51fd58 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts @@ -46,11 +46,14 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 10, - transactionCoordinates: [ - { x: 1, y: 1 }, - { x: 2, y: 2 }, - { x: 3, y: 3 }, - ], + transactionPerMinute: { + value: 2, + timeseries: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ], + }, }) ); const response = await fetchObservabilityOverviewPageData(params); @@ -81,7 +84,7 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionCoordinates: [], + transactionPerMinute: { value: null, timeseries: [] }, }) ); const response = await fetchObservabilityOverviewPageData(params); @@ -108,7 +111,10 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + transactionPerMinute: { + value: 0, + timeseries: [{ x: 1 }, { x: 2 }, { x: 3 }], + }, }) ); const response = await fetchObservabilityOverviewPageData(params); diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index 55ead8d942ac..3a02efd05e5a 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { mean } from 'lodash'; import { ApmFetchDataResponse, FetchDataParams, @@ -31,7 +30,7 @@ export const fetchObservabilityOverviewPageData = async ({ }, }); - const { serviceCount, transactionCoordinates } = data; + const { serviceCount, transactionPerMinute } = data; return { appLink: `/app/apm/services?rangeFrom=${relativeTime.start}&rangeTo=${relativeTime.end}`, @@ -42,17 +41,12 @@ export const fetchObservabilityOverviewPageData = async ({ }, transactions: { type: 'number', - value: - mean( - transactionCoordinates - .map(({ y }) => y) - .filter((y) => y && isFinite(y)) - ) || 0, + value: transactionPerMinute.value || 0, }, }, series: { transactions: { - coordinates: transactionCoordinates, + coordinates: transactionPerMinute.timeseries, }, }, }; diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js index 613435fe186b..fed938119c4a 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js @@ -22,7 +22,7 @@ const { kibanaRoot, tsconfigTpl, filesToIgnore } = require('./paths'); const { unoptimizeTsConfig } = require('./unoptimize'); async function prepareBaseTsConfig() { - const baseConfigFilename = path.resolve(kibanaRoot, 'tsconfig.project.json'); + const baseConfigFilename = path.resolve(kibanaRoot, 'tsconfig.base.json'); const config = json5.parse(await readFile(baseConfigFilename, 'utf-8')); await writeFile( diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js index b501ec3a8eed..dbc207c9e6d2 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js @@ -12,7 +12,7 @@ const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); const filesToIgnore = [ path.resolve(kibanaRoot, 'tsconfig.json'), - path.resolve(kibanaRoot, 'tsconfig.project.json'), + path.resolve(kibanaRoot, 'tsconfig.base.json'), path.resolve(kibanaRoot, 'x-pack/plugins/apm', 'tsconfig.json'), ]; diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts deleted file mode 100644 index aac18e2bdfe4..000000000000 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { rangeQuery } from '../../../server/utils/queries'; -import { Coordinates } from '../../../../observability/typings/common'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -import { calculateThroughput } from '../helpers/calculate_throughput'; -import { withApmSpan } from '../../utils/with_apm_span'; - -export function getTransactionCoordinates({ - setup, - bucketSize, - searchAggregatedTransactions, -}: { - setup: Setup & SetupTimeRange; - bucketSize: string; - searchAggregatedTransactions: boolean; -}): Promise { - return withApmSpan( - 'observability_overview_get_transaction_distribution', - async () => { - const { apmEventClient, start, end } = setup; - - const { aggregations } = await apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: rangeQuery(start, end), - }, - }, - aggs: { - distribution: { - date_histogram: { - field: '@timestamp', - fixed_interval: bucketSize, - min_doc_count: 0, - }, - }, - }, - }, - }); - - return ( - aggregations?.distribution.buckets.map((bucket) => ({ - x: bucket.key, - y: calculateThroughput({ start, end, value: bucket.doc_count }), - })) || [] - ); - } - ); -} diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts new file mode 100644 index 000000000000..da8ac7c50b59 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../common/transaction_types'; +import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; +import { rangeQuery } from '../../../server/utils/queries'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; +import { calculateThroughput } from '../helpers/calculate_throughput'; +import { withApmSpan } from '../../utils/with_apm_span'; + +export function getTransactionsPerMinute({ + setup, + bucketSize, + searchAggregatedTransactions, +}: { + setup: Setup & SetupTimeRange; + bucketSize: string; + searchAggregatedTransactions: boolean; +}) { + return withApmSpan( + 'observability_overview_get_transactions_per_minute', + async () => { + const { apmEventClient, start, end } = setup; + + const { aggregations } = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: rangeQuery(start, end), + }, + }, + aggs: { + transactionType: { + terms: { + field: TRANSACTION_TYPE, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize, + min_doc_count: 0, + }, + aggs: { + throughput: { rate: { unit: 'minute' as const } }, + }, + }, + }, + }, + }, + }, + }); + + if (!aggregations || !aggregations.transactionType.buckets) { + return { value: undefined, timeseries: [] }; + } + + const topTransactionTypeBucket = + aggregations.transactionType.buckets.find( + ({ key: transactionType }) => + transactionType === TRANSACTION_REQUEST || + transactionType === TRANSACTION_PAGE_LOAD + ) || aggregations.transactionType.buckets[0]; + + return { + value: calculateThroughput({ + start, + end, + value: topTransactionTypeBucket?.doc_count || 0, + }), + timeseries: + topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.throughput.value, + })) || [], + }; + } + ); +} diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index 13e70a2043cf..87bc97d34698 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -120,6 +120,7 @@ export function createApi() { return response.ok({ body }); } catch (error) { + logger.error(error); const opts = { statusCode: 500, body: { diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index b9c0a76b6fb9..1aac2c09d01c 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceCount } from '../lib/observability_overview/get_service_count'; -import { getTransactionCoordinates } from '../lib/observability_overview/get_transaction_coordinates'; +import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute'; import { getHasData } from '../lib/observability_overview/has_data'; import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; @@ -39,18 +39,18 @@ export const observabilityOverviewRoute = createRoute({ ); return withApmSpan('observability_overview', async () => { - const [serviceCount, transactionCoordinates] = await Promise.all([ + const [serviceCount, transactionPerMinute] = await Promise.all([ getServiceCount({ setup, searchAggregatedTransactions, }), - getTransactionCoordinates({ + getTransactionsPerMinute({ setup, bucketSize, searchAggregatedTransactions, }), ]); - return { serviceCount, transactionCoordinates }; + return { serviceCount, transactionPerMinute }; }); }, }); diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index ae2085dc2400..ffbf11c23f63 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/banners/tsconfig.json b/x-pack/plugins/banners/tsconfig.json index 6c4c80173208..85608a8a78ad 100644 --- a/x-pack/plugins/banners/tsconfig.json +++ b/x-pack/plugins/banners/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/beats_management/tsconfig.json b/x-pack/plugins/beats_management/tsconfig.json index 398438712b26..ad68cc900e63 100644 --- a/x-pack/plugins/beats_management/tsconfig.json +++ b/x-pack/plugins/beats_management/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index c14e8340957a..cff1a3e7fa8b 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -13,6 +13,7 @@ "expressions", "features", "inspector", + "presentationUtil", "uiActions" ], "optionalPlugins": [ diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 66b02bdc1640..f910aff9a83f 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -56,17 +56,20 @@ export const renderApp = ( { element }: AppMountParameters, canvasStore: Store ) => { + const { presentationUtil } = plugins; element.classList.add('canvas'); element.classList.add('canvasContainerWrapper'); ReactDOM.render( - - - - - + + + + + + + , element diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss b/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss index 2ed6884542b1..620e0eb113d3 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss @@ -66,7 +66,7 @@ text-decoration: none; .canvasPageManager__pagePreview { - @include euiBottomShadowMedium($opacity: .3); + @include euiBottomShadowMedium; } .canvasPageManager__controls { diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss b/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss index 9266273406b8..e770f1092755 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss @@ -1,5 +1,5 @@ .canvasPage { - @include euiBottomShadowFlat($opacity: .4); + @include euiBottomShadowFlat; z-index: initial; position: absolute; top: 0; diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 6871c8d98b8f..486cd03eb9dd 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -27,6 +27,7 @@ import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; +import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getPluginApi, CanvasApi } from './plugin_api'; import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; export { CoreStart, CoreSetup }; @@ -51,6 +52,7 @@ export interface CanvasStartDeps { inspector: InspectorStart; uiActions: UiActionsStart; charts: ChartsPluginStart; + presentationUtil: PresentationUtilPluginStart; } /** diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index 6e74b5ac9862..3865d98caf2b 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -35,6 +35,7 @@ export const useEmbeddablesService = () => useServices().embeddables; export const useExpressionsService = () => useServices().expressions; export const useNotifyService = () => useServices().notify; export const useNavLinkService = () => useServices().navLink; +export const useLabsService = () => useServices().labs; export const withServices = (type: ComponentType) => { const EnhancedType: FC = (props) => @@ -53,6 +54,7 @@ export const ServicesProvider: FC<{ notify: specifiedProviders.notify.getService(), platform: specifiedProviders.platform.getService(), navLink: specifiedProviders.navLink.getService(), + labs: specifiedProviders.labs.getService(), }; return {children}; }; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index 7452352fc0ef..9bfc41a782ed 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -13,6 +13,7 @@ import { platformServiceFactory } from './platform'; import { navLinkServiceFactory } from './nav_link'; import { embeddablesServiceFactory } from './embeddables'; import { expressionsServiceFactory } from './expressions'; +import { labsServiceFactory } from './labs'; export { NotifyService } from './notify'; export { PlatformService } from './platform'; @@ -78,6 +79,7 @@ export const services = { notify: new CanvasServiceProvider(notifyServiceFactory), platform: new CanvasServiceProvider(platformServiceFactory), navLink: new CanvasServiceProvider(navLinkServiceFactory), + labs: new CanvasServiceProvider(labsServiceFactory), }; export type CanvasServiceProviders = typeof services; @@ -88,6 +90,7 @@ export interface CanvasServices { notify: ServiceFromProvider; platform: ServiceFromProvider; navLink: ServiceFromProvider; + labs: ServiceFromProvider; } export const startServices = async ( diff --git a/x-pack/plugins/canvas/public/services/labs.ts b/x-pack/plugins/canvas/public/services/labs.ts new file mode 100644 index 000000000000..9bc4bea3e35c --- /dev/null +++ b/x-pack/plugins/canvas/public/services/labs.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + projectIDs, + Project, + ProjectID, +} from '../../../../../src/plugins/presentation_util/public'; + +import { CanvasServiceFactory } from '.'; + +export interface CanvasLabsService { + getProject: (id: ProjectID) => Project; + getProjects: () => Record; +} + +export const labsServiceFactory: CanvasServiceFactory = async ( + _coreSetup, + _coreStart, + _setupPlugins, + startPlugins +) => ({ + projectIDs, + ...startPlugins.presentationUtil.labsService, +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 2565445af2db..91bda2556284 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -10,6 +10,7 @@ import { embeddablesService } from './embeddables'; import { expressionsService } from './expressions'; import { navLinkService } from './nav_link'; import { notifyService } from './notify'; +import { labsService } from './labs'; import { platformService } from './platform'; export const stubs: CanvasServices = { @@ -18,6 +19,7 @@ export const stubs: CanvasServices = { navLink: navLinkService, notify: notifyService, platform: platformService, + labs: labsService, }; export const startServices = async (providedServices: Partial = {}) => { diff --git a/x-pack/plugins/canvas/public/services/stubs/labs.ts b/x-pack/plugins/canvas/public/services/stubs/labs.ts new file mode 100644 index 000000000000..52168ebeb6f8 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/labs.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CanvasLabsService } from '../labs'; + +const noop = (..._args: any[]): any => {}; + +export const labsService: CanvasLabsService = { + getProject: noop, + getProjects: noop, +}; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx index acf71cad3f3b..b68642d18454 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx @@ -59,7 +59,8 @@ const getWrapper: (name?: WorkpadNames) => ReactWrapper = (name = 'hello') => { return mount(); }; -describe('', () => { +// FLAKY: https://github.com/elastic/kibana/issues/95899 +describe.skip('', () => { test('App renders properly', () => { expect(getWrapper().html()).toMatchSnapshot(); }); diff --git a/x-pack/plugins/canvas/storybook/addon/tsconfig.json b/x-pack/plugins/canvas/storybook/addon/tsconfig.json index b115d1c46546..2ab1856de661 100644 --- a/x-pack/plugins/canvas/storybook/addon/tsconfig.json +++ b/x-pack/plugins/canvas/storybook/addon/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../../tsconfig.project.json", + "extends": "../../../../../tsconfig.base.json", "include": [ "src/**/*.ts", "src/**/*.tsx" diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 679165f0a1b7..487b68ba3542 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/cloud/tsconfig.json b/x-pack/plugins/cloud/tsconfig.json index f6edb9fb7cca..46e81aa7fa08 100644 --- a/x-pack/plugins/cloud/tsconfig.json +++ b/x-pack/plugins/cloud/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/console_extensions/tsconfig.json b/x-pack/plugins/console_extensions/tsconfig.json index edcd46c4fafc..5ad28f230a0b 100644 --- a/x-pack/plugins/console_extensions/tsconfig.json +++ b/x-pack/plugins/console_extensions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/cross_cluster_replication/tsconfig.json b/x-pack/plugins/cross_cluster_replication/tsconfig.json index 156a851abb8d..9c7590b9c255 100644 --- a/x-pack/plugins/cross_cluster_replication/tsconfig.json +++ b/x-pack/plugins/cross_cluster_replication/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/dashboard_enhanced/tsconfig.json b/x-pack/plugins/dashboard_enhanced/tsconfig.json index f6acdddc6f99..567c390edfa5 100644 --- a/x-pack/plugins/dashboard_enhanced/tsconfig.json +++ b/x-pack/plugins/dashboard_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/dashboard_mode/tsconfig.json b/x-pack/plugins/dashboard_mode/tsconfig.json index c4a11959ec3e..6e4ed11ffa7f 100644 --- a/x-pack/plugins/dashboard_mode/tsconfig.json +++ b/x-pack/plugins/dashboard_mode/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts index f9c62069154b..2611f6c9da19 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts @@ -13,9 +13,13 @@ import { EQL_SEARCH_STRATEGY, } from '../../../common'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import type { SavedObjectsClientContract } from 'kibana/server'; import { SearchSessionsConfig, SearchStatus } from './types'; import moment from 'moment'; +import { + SavedObjectsBulkUpdateObject, + SavedObjectsDeleteOptions, + SavedObjectsClientContract, +} from '../../../../../../src/core/server'; describe('getSearchStatus', () => { let mockClient: any; @@ -263,6 +267,45 @@ describe('getSearchStatus', () => { expect(savedObjectsClient.delete).not.toBeCalled(); }); + test('deletes in space', async () => { + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [ + { + id: '123', + namespaces: ['awesome'], + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(2, 'm')), + idMapping: { + 'map-key': { + strategy: ENHANCED_ES_SEARCH_STRATEGY, + id: 'async-id', + }, + }, + }, + }, + ], + total: 1, + } as any); + + await checkRunningSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ); + + expect(savedObjectsClient.delete).toBeCalled(); + + const [, id, opts] = savedObjectsClient.delete.mock.calls[0]; + expect(id).toBe('123'); + expect((opts as SavedObjectsDeleteOptions).namespace).toBe('awesome'); + }); + test('deletes a non persisted, abandoned session', async () => { savedObjectsClient.find.mockResolvedValue({ saved_objects: [ @@ -479,6 +522,50 @@ describe('getSearchStatus', () => { expect(savedObjectsClient.delete).not.toBeCalled(); }); + test('updates in space', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + namespaces: ['awesome'], + attributes: { + status: SearchSessionStatus.IN_PROGRESS, + touched: '123', + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + }; + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [so], + total: 1, + } as any); + + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 200, + }, + }); + + await checkRunningSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ); + + expect(mockClient.asyncSearch.status).toBeCalledWith({ id: 'search-id' }); + const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0]; + const updatedAttributes = updateInput[0] as SavedObjectsBulkUpdateObject; + expect(updatedAttributes.namespace).toBe('awesome'); + }); + test('updates to complete if the search is done', async () => { savedObjectsClient.bulkUpdate = jest.fn(); const so = { @@ -563,7 +650,6 @@ describe('getSearchStatus', () => { config ); const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0]; - const updatedAttributes = updateInput[0].attributes as SearchSessionSavedObjectAttributes; expect(updatedAttributes.status).toBe(SearchSessionStatus.ERROR); expect(updatedAttributes.idMapping['search-hash'].status).toBe(SearchStatus.ERROR); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts index e521c39d7cfd..6e52b17f3680 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts @@ -10,6 +10,7 @@ import { Logger, SavedObjectsClientContract, SavedObjectsFindResult, + SavedObjectsUpdateResponse, } from 'kibana/server'; import moment from 'moment'; import { EMPTY, from } from 'rxjs'; @@ -169,12 +170,20 @@ export async function checkRunningSessions( if (!session.attributes.persisted) { if (isSessionStale(session, config, logger)) { - deleted = true; // delete saved object to free up memory // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session! // Maybe we want to change state to deleted and cleanup later? logger.debug(`Deleting stale session | ${session.id}`); - await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id); + try { + await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, { + namespace: session.namespaces?.[0], + }); + deleted = true; + } catch (e) { + logger.error( + `Error while deleting stale search session ${session.id}: ${e.message}` + ); + } // Send a delete request for each async search to ES Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { @@ -183,8 +192,8 @@ export async function checkRunningSessions( try { await client.asyncSearch.delete({ id: searchInfo.id }); } catch (e) { - logger.debug( - `Error ignored while deleting async_search ${searchInfo.id}: ${e.message}` + logger.error( + `Error while deleting async_search ${searchInfo.id}: ${e.message}` ); } } @@ -202,9 +211,31 @@ export async function checkRunningSessions( if (updatedSessions.length) { // If there's an error, we'll try again in the next iteration, so there's no need to check the output. const updatedResponse = await savedObjectsClient.bulkUpdate( - updatedSessions + updatedSessions.map((session) => ({ + ...session, + namespace: session.namespaces?.[0], + })) + ); + + const success: Array< + SavedObjectsUpdateResponse + > = []; + const fail: Array> = []; + + updatedResponse.saved_objects.forEach((savedObjectResponse) => { + if ('error' in savedObjectResponse) { + fail.push(savedObjectResponse); + logger.error( + `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}` + ); + } else { + success.push(savedObjectResponse); + } + }); + + logger.debug( + `Updating search sessions: success: ${success.length}, fail: ${fail.length}` ); - logger.debug(`Updated ${updatedResponse.saved_objects.length} search sessions`); } }) ) diff --git a/x-pack/plugins/data_enhanced/tsconfig.json b/x-pack/plugins/data_enhanced/tsconfig.json index 5538a2db3e4c..047b9b06516b 100644 --- a/x-pack/plugins/data_enhanced/tsconfig.json +++ b/x-pack/plugins/data_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/discover_enhanced/tsconfig.json b/x-pack/plugins/discover_enhanced/tsconfig.json index 2a055bd0e071..38a55e557909 100644 --- a/x-pack/plugins/discover_enhanced/tsconfig.json +++ b/x-pack/plugins/discover_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json index 99aea16a9aab..50fe41c49b0c 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json +++ b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.project.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/embeddable_enhanced/tsconfig.json b/x-pack/plugins/embeddable_enhanced/tsconfig.json index 32754f2fd552..6e9eb69585cb 100644 --- a/x-pack/plugins/embeddable_enhanced/tsconfig.json +++ b/x-pack/plugins/embeddable_enhanced/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/encrypted_saved_objects/tsconfig.json b/x-pack/plugins/encrypted_saved_objects/tsconfig.json index 9eae8b7366be..2b51b313d34f 100644 --- a/x-pack/plugins/encrypted_saved_objects/tsconfig.json +++ b/x-pack/plugins/encrypted_saved_objects/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index 133f704fd59a..2325ddcf2b27 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -19,6 +19,7 @@ export const mockKibanaValues = { history: mockHistory, navigateToUrl: jest.fn(), setBreadcrumbs: jest.fn(), + setChromeIsVisible: jest.fn(), setDocTitle: jest.fn(), renderHeaderActions: jest.fn(), }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts new file mode 100644 index 000000000000..6106cb049c7a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const mockApiLog = { + timestamp: '1970-01-01T12:00:00.000Z', + http_method: 'POST', + status: 200, + user_agent: 'Mozilla/5.0', + full_request_path: '/api/as/v1/engines/national-parks-demo/search.json', + request_body: '{"query":"test search"}', + response_body: + '{"meta":{"page":{"current":1,"total_pages":0,"total_results":0,"size":20}},"results":[]}', +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx new file mode 100644 index 000000000000..6bebeee80465 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { mockApiLog } from '../__mocks__/api_log.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFlyout, EuiBadge } from '@elastic/eui'; + +import { ApiLogFlyout, ApiLogHeading } from './api_log_flyout'; + +describe('ApiLogFlyout', () => { + const values = { + isFlyoutOpen: true, + apiLog: mockApiLog, + }; + const actions = { + closeFlyout: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('h2').text()).toEqual('Request details'); + expect(wrapper.find(ApiLogHeading).last().dive().find('h3').text()).toEqual('Response body'); + expect(wrapper.find(EuiBadge).prop('children')).toEqual('POST'); + }); + + it('closes the flyout', () => { + const wrapper = shallow(); + + wrapper.find(EuiFlyout).simulate('close'); + expect(actions.closeFlyout).toHaveBeenCalled(); + }); + + it('does not render if the flyout is not open', () => { + setMockValues({ ...values, isFlyoutOpen: false }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('does not render if a current apiLog has not been set', () => { + setMockValues({ ...values, apiLog: null }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx new file mode 100644 index 000000000000..dd53e997da0f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiPortal, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiBadge, + EuiHealth, + EuiText, + EuiCode, + EuiCodeBlock, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { getStatusColor, attemptToFormatJson } from '../utils'; + +import { ApiLogLogic } from './'; + +export const ApiLogFlyout: React.FC = () => { + const { isFlyoutOpen, apiLog } = useValues(ApiLogLogic); + const { closeFlyout } = useActions(ApiLogLogic); + + if (!isFlyoutOpen) return null; + if (!apiLog) return null; + + return ( + + + + +

    + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.flyout.title', { + defaultMessage: 'Request details', + })} +

    +
    +
    + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.methodTitle', { + defaultMessage: 'Method', + })} + +
    + {apiLog.http_method} +
    +
    + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.statusTitle', { + defaultMessage: 'Status', + })} + + {apiLog.status} + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.timestampTitle', { + defaultMessage: 'Timestamp', + })} + + {apiLog.timestamp} + +
    + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.userAgentTitle', { + defaultMessage: 'User agent', + })} + + + {apiLog.user_agent} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.requestPathTitle', { + defaultMessage: 'Request path', + })} + + + {apiLog.full_request_path} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.requestBodyTitle', { + defaultMessage: 'Request body', + })} + + + {attemptToFormatJson(apiLog.request_body)} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.responseBodyTitle', { + defaultMessage: 'Response body', + })} + + + {attemptToFormatJson(apiLog.response_body)} + +
    +
    +
    + ); +}; + +export const ApiLogHeading: React.FC = ({ children }) => ( + +

    {children}

    +
    +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx new file mode 100644 index 000000000000..2b7ca7510e8e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter } from '../../../../__mocks__'; +import { mockApiLog } from '../__mocks__/api_log.mock'; + +import { ApiLogLogic } from './'; + +describe('ApiLogLogic', () => { + const { mount } = new LogicMounter(ApiLogLogic); + + const DEFAULT_VALUES = { + isFlyoutOpen: false, + apiLog: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(ApiLogLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('openFlyout', () => { + it('sets isFlyoutOpen to true & sets the current apiLog', () => { + mount({ isFlyoutOpen: false, apiLog: null }); + ApiLogLogic.actions.openFlyout(mockApiLog); + + expect(ApiLogLogic.values).toEqual({ + ...DEFAULT_VALUES, + isFlyoutOpen: true, + apiLog: mockApiLog, + }); + }); + }); + + describe('closeFlyout', () => { + it('sets isFlyoutOpen to false & resets the current apiLog', () => { + mount({ isFlyoutOpen: true, apiLog: mockApiLog }); + ApiLogLogic.actions.closeFlyout(); + + expect(ApiLogLogic.values).toEqual({ + ...DEFAULT_VALUES, + isFlyoutOpen: false, + apiLog: null, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts new file mode 100644 index 000000000000..8b7c5f70f605 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { ApiLog } from '../types'; + +interface ApiLogValues { + isFlyoutOpen: boolean; + apiLog: ApiLog | null; +} + +interface ApiLogActions { + openFlyout(apiLog: ApiLog): { apiLog: ApiLog }; + closeFlyout(): void; +} + +export const ApiLogLogic = kea>({ + path: ['enterprise_search', 'app_search', 'api_log_logic'], + actions: () => ({ + openFlyout: (apiLog) => ({ apiLog }), + closeFlyout: true, + }), + reducers: () => ({ + isFlyoutOpen: [ + false, + { + openFlyout: () => true, + closeFlyout: () => false, + }, + ], + apiLog: [ + null, + { + openFlyout: (_, { apiLog }) => apiLog, + closeFlyout: () => null, + }, + ], + }), +}); diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts similarity index 72% rename from x-pack/plugins/security_solution/server/graphql/hosts/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts index 400405509b55..dcf949d9bf22 100644 --- a/x-pack/plugins/security_solution/server/graphql/hosts/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { createHostsResolvers } from './resolvers'; -export { hostsSchema } from './schema.gql'; +export { ApiLogFlyout } from './api_log_flyout'; +export { ApiLogLogic } from './api_log_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx index 8ca15906783f..4690911fad77 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx @@ -26,6 +26,7 @@ import { Loading } from '../../../shared/loading'; import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention'; +import { ApiLogFlyout } from './api_log'; import { ApiLogsTable, NewApiEventsPrompt } from './components'; import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants'; @@ -75,6 +76,7 @@ export const ApiLogs: React.FC = ({ engineBreadcrumb }) => { + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts index 7b3ee80668ac..2eda4c6323fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts @@ -6,6 +6,7 @@ */ import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; +import { mockApiLog } from './__mocks__/api_log.mock'; import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; @@ -29,17 +30,7 @@ describe('ApiLogsLogic', () => { }; const MOCK_API_RESPONSE = { - results: [ - { - timestamp: '1970-01-01T12:00:00.000Z', - http_method: 'POST', - status: 200, - user_agent: 'some browser agent string', - full_request_path: '/api/as/v1/engines/national-parks-demo/search.json', - request_body: '{"someMockRequest":"hello"}', - response_body: '{"someMockResponse":"world"}', - }, - ], + results: [mockApiLog, mockApiLog], meta: { page: { current: 1, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx index 99fce81ca348..768295ec1389 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx @@ -53,6 +53,7 @@ describe('ApiLogsTable', () => { }; const actions = { onPaginate: jest.fn(), + openFlyout: jest.fn(), }; beforeEach(() => { @@ -86,7 +87,7 @@ describe('ApiLogsTable', () => { expect(wrapper.find(EuiButtonEmpty)).toHaveLength(3); wrapper.find('[data-test-subj="ApiLogsTableDetailsButton"]').first().simulate('click'); - // TODO: API log details flyout + expect(actions.openFlyout).toHaveBeenCalled(); }); it('renders an empty prompt if no items are passed', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx index 8ebcc4350f7f..5ecf8e1ba333 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx @@ -22,6 +22,7 @@ import { FormattedRelative } from '@kbn/i18n/react'; import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; +import { ApiLogLogic } from '../api_log'; import { ApiLogsLogic } from '../index'; import { ApiLog } from '../types'; import { getStatusColor } from '../utils'; @@ -34,6 +35,7 @@ interface Props { export const ApiLogsTable: React.FC = ({ hasPagination }) => { const { dataLoading, apiLogs, meta } = useValues(ApiLogsLogic); const { onPaginate } = useActions(ApiLogsLogic); + const { openFlyout } = useActions(ApiLogLogic); const columns: Array> = [ { @@ -81,7 +83,7 @@ export const ApiLogsTable: React.FC = ({ hasPagination }) => { size="s" className="apiLogDetailButton" data-test-subj="ApiLogsTableDetailsButton" - // TODO: flyout onclick + onClick={() => openFlyout(apiLog)} > {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.detailsButtonLabel', { defaultMessage: 'Details', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts index 183956e51d8d..568026dab231 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts @@ -7,5 +7,6 @@ export { API_LOGS_TITLE } from './constants'; export { ApiLogsTable, NewApiEventsPrompt } from './components'; +export { ApiLogFlyout } from './api_log'; export { ApiLogs } from './api_logs'; export { ApiLogsLogic } from './api_logs_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts index f9b6dcea2cbf..ac464e2af353 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { getDateString, getStatusColor } from './utils'; +import dedent from 'dedent'; + +import { getDateString, getStatusColor, attemptToFormatJson } from './utils'; describe('getDateString', () => { const mockDate = jest @@ -32,3 +34,20 @@ describe('getStatusColor', () => { expect(getStatusColor(503)).toEqual('danger'); }); }); + +describe('attemptToFormatJson', () => { + it('takes an unformatted JSON string and correctly newlines/indents it', () => { + expect(attemptToFormatJson('{"hello":"world","lorem":{"ipsum":"dolor","sit":"amet"}}')) + .toEqual(dedent`{ + "hello": "world", + "lorem": { + "ipsum": "dolor", + "sit": "amet" + } + }`); + }); + + it('returns the original content if it is not properly formatted JSON', () => { + expect(attemptToFormatJson('{invalid json}')).toEqual('{invalid json}'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts index 3217a1561ce7..7e5f19686f13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts @@ -19,3 +19,13 @@ export const getStatusColor = (status: number) => { if (status >= 500) color = 'danger'; return color; }; + +export const attemptToFormatJson = (possibleJson: string) => { + try { + // it is JSON, we can format it with newlines/indentation + return JSON.stringify(JSON.parse(possibleJson), null, 2); + } catch { + // if it's not JSON, we return the original content + return possibleJson; + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx index 3686f380407e..18f27c3a1e83 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx @@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { ENGINE_API_LOGS_PATH } from '../../../routes'; -import { ApiLogsLogic, ApiLogsTable, NewApiEventsPrompt } from '../../api_logs'; +import { ApiLogsLogic, ApiLogsTable, NewApiEventsPrompt, ApiLogFlyout } from '../../api_logs'; import { RECENT_API_EVENTS } from '../../api_logs/constants'; import { DataPanel } from '../../data_panel'; import { generateEnginePath } from '../../engine'; @@ -46,6 +46,7 @@ export const RecentApiLogs: React.FC = () => { hasBorder > + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 38db5c60e98a..7f4373835f8d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -17,6 +17,8 @@ import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chro import { RESULT_SETTINGS_TITLE } from './constants'; import { ResultSettingsTable } from './result_settings_table'; +import { SampleResponse } from './sample_response'; + import { ResultSettingsLogic } from '.'; interface Props { @@ -40,7 +42,7 @@ export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { -
    TODO
    +
    diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx index 145654be2046..dc91b5039a3c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx @@ -10,6 +10,7 @@ import React, { useMemo } from 'react'; import { useValues, useActions } from 'kea'; import { EuiTableRow, EuiTableRowCell, EuiCheckbox, EuiTableRowCellCheckbox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ResultSettingsLogic } from '..'; import { FieldResultSetting } from '../types'; @@ -33,6 +34,10 @@ export const NonTextFieldsBody: React.FC = () => { { { { { + const actions = { + queryChanged: jest.fn(), + getSearchResults: jest.fn(), + }; + + const values = { + reducedServerResultFields: {}, + query: 'foo', + response: { + bar: 'baz', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + setMockValues(values); + }); + + it('renders a text box with the current user "query" value from state', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo'); + }); + + it('updates the "query" value in state when a user updates the text in the text box', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldSearch).simulate('change', { target: { value: 'bar' } }); + expect(actions.queryChanged).toHaveBeenCalledWith('bar'); + }); + + it('will call getSearchResults with the current value of query and reducedServerResultFields in a useEffect, which updates the displayed response', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo'); + }); + + it('renders the response from the given user "query" in a code block', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual('{\n "bar": "baz"\n}'); + }); + + it('renders a plain old string in the code block if the response is a string', () => { + setMockValues({ + response: 'No results.', + }); + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual('No results.'); + }); + + it('will not render a code block at all if there is no response yet', () => { + setMockValues({ + response: null, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).exists()).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx new file mode 100644 index 000000000000..ae91b9648356 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiCodeBlock, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ResultSettingsLogic } from '../result_settings_logic'; + +import { SampleResponseLogic } from './sample_response_logic'; + +export const SampleResponse: React.FC = () => { + const { reducedServerResultFields } = useValues(ResultSettingsLogic); + + const { query, response } = useValues(SampleResponseLogic); + const { queryChanged, getSearchResults } = useActions(SampleResponseLogic); + + useEffect(() => { + getSearchResults(query, reducedServerResultFields); + }, [query, reducedServerResultFields]); + + return ( + + + + +

    + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponseTitle', + { defaultMessage: 'Sample response' } + )} +

    +
    +
    + + {/* TODO */} + +
    + + queryChanged(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.inputPlaceholder', + { defaultMessage: 'Type a search query to test a response...' } + )} + data-test-subj="ResultSettingsQuerySampleResponse" + /> + + {!!response && ( + + {typeof response === 'string' ? response : JSON.stringify(response, null, 2)} + + )} +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts new file mode 100644 index 000000000000..79379306c161 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter, mockHttpValues } from '../../../../__mocks__'; +import '../../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { SampleResponseLogic } from './sample_response_logic'; + +describe('SampleResponseLogic', () => { + const { mount } = new LogicMounter(SampleResponseLogic); + const { http } = mockHttpValues; + + const DEFAULT_VALUES = { + query: '', + response: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + }); + }); + + describe('actions', () => { + describe('queryChanged', () => { + it('updates the query', () => { + mount({ + query: '', + }); + + SampleResponseLogic.actions.queryChanged('foo'); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + query: 'foo', + }); + }); + }); + + describe('getSearchResultsSuccess', () => { + it('sets the response from a search API request', () => { + mount({ + response: null, + }); + + SampleResponseLogic.actions.getSearchResultsSuccess({}); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + response: {}, + }); + }); + }); + + describe('getSearchResultsFailure', () => { + it('sets a string response from a search API request', () => { + mount({ + response: null, + }); + + SampleResponseLogic.actions.getSearchResultsFailure('An error occured.'); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + response: 'An error occured.', + }); + }); + }); + }); + + describe('listeners', () => { + describe('getSearchResults', () => { + beforeAll(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + + it('makes a search API request and calls getSearchResultsSuccess with the first result of the response', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + http.post.mockReturnValue( + Promise.resolve({ + results: [ + { id: { raw: 'foo' }, _meta: {} }, + { id: { raw: 'bar' }, _meta: {} }, + { id: { raw: 'baz' }, _meta: {} }, + ], + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).toHaveBeenCalledWith({ + // Note that the _meta field was stripped from the result + id: { raw: 'foo' }, + }); + }); + + it('calls getSearchResultsSuccess with a "No Results." message if there are no results', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + http.post.mockReturnValue( + Promise.resolve({ + results: [], + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).toHaveBeenCalledWith( + 'No results.' + ); + }); + + it('handles 500 errors by setting a generic error response and showing a flash message error', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + const error = { + response: { + status: 500, + }, + }; + + http.post.mockReturnValueOnce(Promise.reject(error)); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith( + 'An error occured.' + ); + }); + + it('handles 400 errors by setting the response, but does not show a flash error message', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + http.post.mockReturnValueOnce( + Promise.reject({ + response: { + status: 400, + }, + body: { + attributes: { + errors: ['A validation error occurred.'], + }, + }, + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith({ + errors: ['A validation error occurred.'], + }); + }); + + it('sets a generic message on a 400 error if no custom message is provided in the response', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + http.post.mockReturnValueOnce( + Promise.reject({ + response: { + status: 400, + }, + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith( + 'An error occured.' + ); + }); + + it('does nothing if an empty object is passed for the resultFields parameter', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + SampleResponseLogic.actions.getSearchResults('foo', {}); + + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts new file mode 100644 index 000000000000..808a7ec9c65d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { HttpLogic } from '../../../../shared/http'; +import { EngineLogic } from '../../engine'; + +import { SampleSearchResponse, ServerFieldResultSettingObject } from '../types'; + +const NO_RESULTS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.noResultsMessage', + { defaultMessage: 'No results.' } +); + +const ERROR_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.errorMessage', + { defaultMessage: 'An error occured.' } +); + +interface SampleResponseValues { + query: string; + response: SampleSearchResponse | string | null; +} + +interface SampleResponseActions { + queryChanged: (query: string) => { query: string }; + getSearchResultsSuccess: ( + response: SampleSearchResponse | string + ) => { response: SampleSearchResponse | string }; + getSearchResultsFailure: (response: string) => { response: string }; + getSearchResults: ( + query: string, + resultFields: ServerFieldResultSettingObject + ) => { query: string; resultFields: ServerFieldResultSettingObject }; +} + +export const SampleResponseLogic = kea>({ + path: ['enterprise_search', 'app_search', 'sample_response_logic'], + actions: { + queryChanged: (query) => ({ query }), + getSearchResultsSuccess: (response) => ({ response }), + getSearchResultsFailure: (response) => ({ response }), + getSearchResults: (query, resultFields) => ({ query, resultFields }), + }, + reducers: { + query: ['', { queryChanged: (_, { query }) => query }], + response: [ + null, + { + getSearchResultsSuccess: (_, { response }) => response, + getSearchResultsFailure: (_, { response }) => response, + }, + ], + }, + listeners: ({ actions }) => ({ + getSearchResults: async ({ query, resultFields }, breakpoint) => { + if (Object.keys(resultFields).length < 1) return; + await breakpoint(250); + + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/sample_response_search`; + + try { + const response = await http.post(url, { + body: JSON.stringify({ + query, + result_fields: resultFields, + }), + }); + + const result = response.results?.[0]; + actions.getSearchResultsSuccess( + result ? { ...result, _meta: undefined } : NO_RESULTS_MESSAGE + ); + } catch (e) { + if (e.response.status >= 500) { + // 4XX Validation errors are expected, as a user could enter something like 2 as a size, which is out of valid range. + // In this case, we simply render the message from the server as the response. + // + // 5xx Server errors are unexpected, and need to be reported in a flash message. + flashAPIErrors(e); + actions.getSearchResultsFailure(ERROR_MESSAGE); + } else { + actions.getSearchResultsFailure(e.body?.attributes || ERROR_MESSAGE); + } + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts index 96bf277314a7..18843112f46b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { FieldValue } from '../result/types'; + export enum OpenModal { None, ConfirmResetModal, @@ -35,3 +37,5 @@ export interface FieldResultSetting { } export type FieldResultSettingObject = Record; + +export type SampleSearchResponse = Record; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 155ff5b92ba2..c2bf77751528 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -49,6 +49,7 @@ export const renderApp = ( history: params.history, navigateToUrl: core.application.navigateToUrl, setBreadcrumbs: core.chrome.setBreadcrumbs, + setChromeIsVisible: core.chrome.setIsVisible, setDocTitle: core.chrome.docTitle.change, renderHeaderActions: (HeaderActions) => params.setHeaderActionMenu((el) => renderHeaderActions(HeaderActions, store, el)), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 8015d22f7c44..2bef7d373f16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -24,6 +24,7 @@ interface KibanaLogicProps { charts: ChartsPluginStart; navigateToUrl: ApplicationStart['navigateToUrl']; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; + setChromeIsVisible(isVisible: boolean): void; setDocTitle(title: string): void; renderHeaderActions(HeaderActions: FC): void; } @@ -47,6 +48,7 @@ export const KibanaLogic = kea>({ {}, ], setBreadcrumbs: [props.setBreadcrumbs, {}], + setChromeIsVisible: [props.setChromeIsVisible, {}], setDocTitle: [props.setDocTitle, {}], renderHeaderActions: [props.renderHeaderActions, {}], }), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 48bdcd6551b6..a2c0ec18def4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -57,11 +57,13 @@ describe('WorkplaceSearchConfigured', () => { setMockActions({ initializeAppData, setContext }); }); - it('renders layout and header actions', () => { + it('renders layout, chrome, and header actions', () => { const wrapper = shallow(); expect(wrapper.find(Layout).first().prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(OverviewMVP)).toHaveLength(1); + + expect(mockKibanaValues.setChromeIsVisible).toHaveBeenCalledWith(true); expect(mockKibanaValues.renderHeaderActions).toHaveBeenCalledWith(WorkplaceSearchHeaderActions); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index c269a987dc09..7a76de43be41 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -53,7 +53,7 @@ export const WorkplaceSearch: React.FC = (props) => { export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData, setContext } = useActions(AppLogic); - const { renderHeaderActions } = useValues(KibanaLogic); + const { renderHeaderActions, setChromeIsVisible } = useValues(KibanaLogic); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); const { pathname } = useLocation(); @@ -66,11 +66,13 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ - const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + useEffect(() => { + const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - const isOrganization = !pathname.match(personalSourceUrlRegex); - setContext(isOrganization); + setContext(isOrganization); + setChromeIsVisible(isOrganization); + }, [pathname]); useEffect(() => { if (!hasInitialized) { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 50f6596a860c..9e514d7c7349 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -73,11 +73,11 @@ export const ADD_GMAIL_PATH = `${SOURCES_PATH}/add/gmail`; export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google_drive`; export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira_cloud`; export const ADD_JIRA_SERVER_PATH = `${SOURCES_PATH}/add/jira_server`; -export const ADD_ONEDRIVE_PATH = `${SOURCES_PATH}/add/onedrive`; +export const ADD_ONEDRIVE_PATH = `${SOURCES_PATH}/add/one_drive`; export const ADD_SALESFORCE_PATH = `${SOURCES_PATH}/add/salesforce`; export const ADD_SALESFORCE_SANDBOX_PATH = `${SOURCES_PATH}/add/salesforce_sandbox`; export const ADD_SERVICENOW_PATH = `${SOURCES_PATH}/add/servicenow`; -export const ADD_SHAREPOINT_PATH = `${SOURCES_PATH}/add/sharepoint`; +export const ADD_SHAREPOINT_PATH = `${SOURCES_PATH}/add/share_point`; export const ADD_SLACK_PATH = `${SOURCES_PATH}/add/slack`; export const ADD_ZENDESK_PATH = `${SOURCES_PATH}/add/zendesk`; export const ADD_CUSTOM_PATH = `${SOURCES_PATH}/add/custom`; @@ -108,11 +108,11 @@ export const EDIT_GMAIL_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/gmail/edit`; export const EDIT_GOOGLE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/google_drive/edit`; export const EDIT_JIRA_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_cloud/edit`; export const EDIT_JIRA_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_server/edit`; -export const EDIT_ONEDRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/onedrive/edit`; +export const EDIT_ONEDRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/one_drive/edit`; export const EDIT_SALESFORCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce/edit`; export const EDIT_SALESFORCE_SANDBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce_sandbox/edit`; export const EDIT_SERVICENOW_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/servicenow/edit`; -export const EDIT_SHAREPOINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/sharepoint/edit`; +export const EDIT_SHAREPOINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/share_point/edit`; export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`; export const EDIT_ZENDESK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/zendesk/edit`; export const EDIT_CUSTOM_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/custom/edit`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx index 99cebd5ded58..bf0c5471f7b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx @@ -33,7 +33,7 @@ export const SourceSubNav: React.FC = () => { const isCustom = serviceType === CUSTOM_SERVICE_TYPE; return ( - <> +
    {NAV.OVERVIEW} @@ -53,6 +53,6 @@ export const SourceSubNav: React.FC = () => { {NAV.SETTINGS} - +
    ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx index 488eb4b49853..9e3b50ea083e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx @@ -17,6 +17,8 @@ import { EuiCallOut } from '@elastic/eui'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SourceSubNav } from './components/source_sub_nav'; + import { PRIVATE_CAN_CREATE_PAGE_TITLE, PRIVATE_VIEW_ONLY_PAGE_TITLE, @@ -40,6 +42,7 @@ describe('PrivateSourcesLayout', () => { const wrapper = shallow({children}); expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); + expect(wrapper.find(SourceSubNav)).toHaveLength(1); }); it('uses correct title and description when private sources are enabled', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx index bdc2421432c8..2a6281075dc4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx @@ -14,6 +14,8 @@ import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { AppLogic } from '../../app_logic'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SourceSubNav } from './components/source_sub_nav'; + import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING, PRIVATE_CAN_CREATE_PAGE_TITLE, @@ -49,6 +51,7 @@ export const PrivateSourcesLayout: React.FC = ({ + {readOnlyMode && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index d20d0576d11c..a9712cc4e1dc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -214,7 +214,7 @@ describe('SourceLogic', () => { SourceLogic.actions.initializeFederatedSummary(contentSource.id); expect(http.get).toHaveBeenCalledWith( - '/api/workplace_search/org/sources/123/federated_summary' + '/api/workplace_search/account/sources/123/federated_summary' ); await promise; expect(onUpdateSummarySpy).toHaveBeenCalledWith(contentSource.summary); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 72700ce42c75..3da90c4fc773 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -156,7 +156,7 @@ export const SourceLogic = kea>({ } }, initializeFederatedSummary: async ({ sourceId }) => { - const route = `/api/workplace_search/org/sources/${sourceId}/federated_summary`; + const route = `/api/workplace_search/account/sources/${sourceId}/federated_summary`; try { const response = await HttpLogic.values.http.get(route); actions.onUpdateSummary(response.summary); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss index f142567fb621..abab139e3236 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss @@ -30,3 +30,9 @@ margin-left: -$sideBarWidth; } } + +.sourcesSubNav { + li { + display: block; + } +} diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index f00e81a5accf..dd1a62d243d0 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -114,6 +114,9 @@ export class EnterpriseSearchPlugin implements Plugin { const { chrome, http } = kibanaDeps.core; chrome.docTitle.change(WORKPLACE_SEARCH_PLUGIN.NAME); + // The Workplace Search Personal dashboard needs the chrome hidden. We hide it globally + // here first to prevent a flash of chrome on the Personal dashboard and unhide it for admin routes. + chrome.setIsVisible(false); await this.getInitialData(http); const pluginData = this.getPluginData(); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts index 8d1a7e3ead37..e38380d60c6e 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts @@ -88,4 +88,48 @@ describe('result settings routes', () => { }); }); }); + + describe('POST /api/app_search/engines/{name}/sample_response_search', () => { + const mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/sample_response_search', + }); + + beforeEach(() => { + registerResultSettingsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + body: { + query: 'test', + result_fields: resultFields, + }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/sample_response_search', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + query: 'test', + result_fields: resultFields, + }, + }; + mockRouter.shouldValidate(request); + }); + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts index 38cb4aa92273..b091ae7a539c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts @@ -45,4 +45,22 @@ export function registerResultSettingsRoutes({ path: '/as/engines/:engineName/result_settings', }) ); + + router.post( + { + path: '/api/app_search/engines/{engineName}/sample_response_search', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + body: schema.object({ + query: schema.string(), + result_fields: schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' })), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/sample_response_search', + }) + ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 8257dd0dc52b..1dd6d859d88a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -22,9 +22,9 @@ const schemaValuesSchema = schema.recordOf( ); const pageSchema = schema.object({ - current: schema.number(), - size: schema.number(), - total_pages: schema.number(), + current: schema.nullable(schema.number()), + size: schema.nullable(schema.number()), + total_pages: schema.nullable(schema.number()), total_results: schema.number(), }); diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index a4f1c55463e7..6b4c50770b49 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/event_log/tsconfig.json b/x-pack/plugins/event_log/tsconfig.json index e21dbc93b7b4..9b7cde10da3d 100644 --- a/x-pack/plugins/event_log/tsconfig.json +++ b/x-pack/plugins/event_log/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/features/tsconfig.json b/x-pack/plugins/features/tsconfig.json index 11e2dbc8f093..1260af55fbff 100644 --- a/x-pack/plugins/features/tsconfig.json +++ b/x-pack/plugins/features/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/file_upload/common/types.ts b/x-pack/plugins/file_upload/common/types.ts index 0fc59e2b525a..11cf4ac3615b 100644 --- a/x-pack/plugins/file_upload/common/types.ts +++ b/x-pack/plugins/file_upload/common/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { ES_FIELD_TYPES } from '../../../../src/plugins/data/common'; export interface HasImportPermission { @@ -83,7 +84,9 @@ export interface ImportResponse { pipelineId?: string; docCount: number; failures: ImportFailure[]; - error?: any; + error?: { + error: estypes.ErrorCause; + }; ingestError?: boolean; } diff --git a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx index 29aed0cd52f7..a3bc2ed082b1 100644 --- a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx +++ b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx @@ -7,19 +7,20 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonIcon, EuiCallOut, EuiCopy, EuiFlexGroup, EuiFlexItem, + EuiLink, EuiSpacer, EuiText, EuiTitle, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { CodeEditor, KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { getHttp, getUiSettings } from '../kibana_services'; +import { getDocLinks, getHttp, getUiSettings } from '../kibana_services'; import { ImportResults } from '../importer'; const services = { @@ -27,8 +28,10 @@ const services = { }; interface Props { + failedPermissionCheck: boolean; importResults?: ImportResults; indexPatternResp?: object; + indexName: string; } export class ImportCompleteView extends Component { @@ -57,9 +60,12 @@ export class ImportCompleteView extends Component { iconType="copy" color="text" data-test-subj={copyButtonDataTestSubj} - aria-label={i18n.translate('xpack.fileUpload.copyButtonAriaLabel', { - defaultMessage: 'Copy to clipboard', - })} + aria-label={i18n.translate( + 'xpack.fileUpload.importComplete.copyButtonAriaLabel', + { + defaultMessage: 'Copy to clipboard', + } + )} /> )} @@ -90,21 +96,65 @@ export class ImportCompleteView extends Component { } _getStatusMsg() { + if (this.props.failedPermissionCheck) { + return ( + +

    + {i18n.translate('xpack.fileUpload.importComplete.permissionFailureMsg', { + defaultMessage: + 'You do not have permission to create or import data into index "{indexName}".', + values: { indexName: this.props.indexName }, + })} +

    + + {i18n.translate('xpack.fileUpload.importComplete.permission.docLink', { + defaultMessage: 'View file import permissions', + })} + +
    + ); + } + if (!this.props.importResults || !this.props.importResults.success) { - return i18n.translate('xpack.fileUpload.uploadFailureMsg', { - defaultMessage: 'File upload failed.', - }); + const errorMsg = + this.props.importResults && this.props.importResults.error + ? i18n.translate('xpack.fileUpload.importComplete.uploadFailureMsgErrorBlock', { + defaultMessage: 'Error: {reason}', + values: { reason: this.props.importResults.error.error.reason }, + }) + : ''; + return ( + +

    {errorMsg}

    +
    + ); } - const successMsg = i18n.translate('xpack.fileUpload.uploadSuccessMsg', { - defaultMessage: 'File upload complete: indexed {numFeatures} features.', + const successMsg = i18n.translate('xpack.fileUpload.importComplete.uploadSuccessMsg', { + defaultMessage: 'Indexed {numFeatures} features.', values: { numFeatures: this.props.importResults.docCount, }, }); const failedFeaturesMsg = this.props.importResults.failures?.length - ? i18n.translate('xpack.fileUpload.failedFeaturesMsg', { + ? i18n.translate('xpack.fileUpload.importComplete.failedFeaturesMsg', { defaultMessage: 'Unable to index {numFailures} features.', values: { numFailures: this.props.importResults.failures.length, @@ -112,47 +162,60 @@ export class ImportCompleteView extends Component { }) : ''; - return `${successMsg} ${failedFeaturesMsg}`; + return ( + +

    {`${successMsg} ${failedFeaturesMsg}`}

    +
    + ); + } + + _renderIndexManagementMsg() { + return this.props.importResults && this.props.importResults.success ? ( + +

    + + + + +

    +
    + ) : null; } render() { return ( - -

    {this._getStatusMsg()}

    -
    + {this._getStatusMsg()} + {this._renderCodeEditor( this.props.importResults, - i18n.translate('xpack.fileUpload.jsonImport.indexingResponse', { + i18n.translate('xpack.fileUpload.importComplete.indexingResponse', { defaultMessage: 'Import response', }), 'indexRespCopyButton' )} {this._renderCodeEditor( this.props.indexPatternResp, - i18n.translate('xpack.fileUpload.jsonImport.indexPatternResponse', { + i18n.translate('xpack.fileUpload.importComplete.indexPatternResponse', { defaultMessage: 'Index pattern response', }), 'indexPatternRespCopyButton' )} - -
    - - - - -
    -
    + {this._renderIndexManagementMsg()}
    ); } diff --git a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx index 371d68443bc2..d73c6e9c5fb3 100644 --- a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx +++ b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx @@ -16,6 +16,7 @@ import { FileUploadComponentProps } from '../lazy_load_bundle'; import { ImportResults } from '../importer'; import { GeoJsonImporter } from '../importer/geojson_importer'; import { Settings } from '../../common'; +import { hasImportPermission } from '../api'; enum PHASE { CONFIGURE = 'CONFIGURE', @@ -31,6 +32,7 @@ function getWritingToIndexMsg(progress: number) { } interface State { + failedPermissionCheck: boolean; geoFieldType: ES_FIELD_TYPES.GEO_POINT | ES_FIELD_TYPES.GEO_SHAPE; importStatus: string; importResults?: ImportResults; @@ -45,6 +47,7 @@ export class JsonUploadAndParse extends Component ); } diff --git a/x-pack/plugins/file_upload/public/kibana_services.ts b/x-pack/plugins/file_upload/public/kibana_services.ts index a604136ca34e..dfe2785e7a2b 100644 --- a/x-pack/plugins/file_upload/public/kibana_services.ts +++ b/x-pack/plugins/file_upload/public/kibana_services.ts @@ -15,6 +15,7 @@ export function setStartServices(core: CoreStart, plugins: FileUploadStartDepend pluginsStart = plugins; } +export const getDocLinks = () => coreStart.docLinks; export const getIndexPatternService = () => pluginsStart.data.indexPatterns; export const getHttp = () => coreStart.http; export const getSavedObjectsClient = () => coreStart.savedObjects.client; diff --git a/x-pack/plugins/file_upload/tsconfig.json b/x-pack/plugins/file_upload/tsconfig.json index 8a982f83632a..887a05af3117 100644 --- a/x-pack/plugins/file_upload/tsconfig.json +++ b/x-pack/plugins/file_upload/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.project.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "composite": true, "outDir": "./target/types", diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx index 9ee0b0a7b29e..bdf49f44f439 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx @@ -48,6 +48,48 @@ export const AgentPolicyActionMenu = memo<{ return ( {(copyAgentPolicyPrompt) => { + const viewPolicyItem = ( + setIsYamlFlyoutOpen(!isYamlFlyoutOpen)} + key="viewPolicy" + > + + + ); + + const menuItems = agentPolicy?.is_managed + ? [viewPolicyItem] + : [ + setIsEnrollmentFlyoutOpen(true)} + key="enrollAgents" + > + + , + viewPolicyItem, + { + copyAgentPolicyPrompt(agentPolicy, onCopySuccess); + }} + key="copyPolicy" + > + + , + ]; return ( <> {isYamlFlyoutOpen ? ( @@ -80,42 +122,7 @@ export const AgentPolicyActionMenu = memo<{ } : undefined } - items={[ - setIsEnrollmentFlyoutOpen(true)} - key="enrollAgents" - > - - , - setIsYamlFlyoutOpen(!isYamlFlyoutOpen)} - key="viewPolicy" - > - - , - { - copyAgentPolicyPrompt(agentPolicy, onCopySuccess); - }} - key="copyPolicy" - > - - , - ]} + items={menuItems} /> ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index c859d585f4d8..de27d5fada75 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -148,12 +148,21 @@ export const AgentBulkActions: React.FunctionComponent<{ }, ]; + const showSelectEverything = + selectionMode === 'manual' && + selectedAgents.length === selectableAgents && + selectableAgents < totalAgents; + + const totalActiveAgents = totalAgents - totalInactiveAgents; + const agentCount = selectionMode === 'manual' ? selectedAgents.length : totalActiveAgents; + const agents = selectionMode === 'manual' ? selectedAgents : currentQuery; + return ( <> {isReassignFlyoutOpen && ( { setIsReassignFlyoutOpen(false); refreshAgents(); @@ -164,10 +173,8 @@ export const AgentBulkActions: React.FunctionComponent<{ {isUnenrollModalOpen && ( { setIsUnenrollModalOpen(false); refreshAgents(); @@ -179,10 +186,8 @@ export const AgentBulkActions: React.FunctionComponent<{ { setIsUpgradeModalOpen(false); refreshAgents(); @@ -230,12 +235,9 @@ export const AgentBulkActions: React.FunctionComponent<{ > @@ -248,9 +250,7 @@ export const AgentBulkActions: React.FunctionComponent<{ - {selectionMode === 'manual' && - selectedAgents.length === selectableAgents && - selectableAgents < totalAgents ? ( + {showSelectEverything ? (