diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 508cd8f9e8007..dde90bf1bc47d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -99,6 +99,7 @@ /src/plugins/dashboard/ @elastic/kibana-presentation /src/plugins/input_control_vis/ @elastic/kibana-presentation /src/plugins/vis_type_markdown/ @elastic/kibana-presentation +/src/plugins/presentation_util/ @elastic/kibana-presentation /test/functional/apps/dashboard/ @elastic/kibana-presentation /x-pack/plugins/canvas/ @elastic/kibana-presentation /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation diff --git a/api_docs/data.json b/api_docs/data.json index 24bc790bbafa7..13e2b402a4afd 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -27594,4 +27594,4 @@ } ] } -} \ No newline at end of file +} diff --git a/api_docs/data_search.json b/api_docs/data_search.json index 6dc7c105051f5..d0eb07083c2f6 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -19470,4 +19470,4 @@ } ] } -} \ No newline at end of file +} diff --git a/api_docs/discover.json b/api_docs/discover.json index adcecddfcc444..267669692051f 100644 --- a/api_docs/discover.json +++ b/api_docs/discover.json @@ -911,21 +911,6 @@ "interfaces": [], "enums": [], "misc": [ - { - "tags": [], - "id": "def-common.AGGS_TERMS_SIZE_SETTING", - "type": "string", - "label": "AGGS_TERMS_SIZE_SETTING", - "description": [], - "source": { - "path": "src/plugins/discover/common/index.ts", - "lineNumber": 11 - }, - "signature": [ - "\"discover:aggs:terms:size\"" - ], - "initialIsOpen": false - }, { "tags": [], "id": "def-common.CONTEXT_DEFAULT_SIZE_SETTING", @@ -934,7 +919,7 @@ "description": [], "source": { "path": "src/plugins/discover/common/index.ts", - "lineNumber": 16 + "lineNumber": 15 }, "signature": [ "\"context:defaultSize\"" @@ -949,7 +934,7 @@ "description": [], "source": { "path": "src/plugins/discover/common/index.ts", - "lineNumber": 17 + "lineNumber": 16 }, "signature": [ "\"context:step\"" @@ -964,7 +949,7 @@ "description": [], "source": { "path": "src/plugins/discover/common/index.ts", - "lineNumber": 18 + "lineNumber": 17 }, "signature": [ "\"context:tieBreakerFields\"" @@ -994,7 +979,7 @@ "description": [], "source": { "path": "src/plugins/discover/common/index.ts", - "lineNumber": 14 + "lineNumber": 13 }, "signature": [ "\"doc_table:hideTimeColumn\"" @@ -1009,7 +994,7 @@ "description": [], "source": { "path": "src/plugins/discover/common/index.ts", - "lineNumber": 19 + "lineNumber": 18 }, "signature": [ "\"doc_table:legacy\"" @@ -1024,7 +1009,7 @@ "description": [], "source": { "path": "src/plugins/discover/common/index.ts", - "lineNumber": 15 + "lineNumber": 14 }, "signature": [ "\"fields:popularLimit\"" @@ -1039,7 +1024,7 @@ "description": [], "source": { "path": "src/plugins/discover/common/index.ts", - "lineNumber": 20 + "lineNumber": 19 }, "signature": [ "\"discover:modifyColumnsOnSwitch\"" @@ -1069,7 +1054,7 @@ "description": [], "source": { "path": "src/plugins/discover/common/index.ts", - "lineNumber": 21 + "lineNumber": 20 }, "signature": [ "\"discover:searchFieldsFromSource\"" @@ -1084,7 +1069,7 @@ "description": [], "source": { "path": "src/plugins/discover/common/index.ts", - "lineNumber": 13 + "lineNumber": 12 }, "signature": [ "\"discover:searchOnPageLoad\"" @@ -1099,7 +1084,7 @@ "description": [], "source": { "path": "src/plugins/discover/common/index.ts", - "lineNumber": 12 + "lineNumber": 11 }, "signature": [ "\"discover:sort:defaultOrder\"" diff --git a/api_docs/expressions.json b/api_docs/expressions.json index ff04fcd03f046..ee496cc7c06a3 100644 --- a/api_docs/expressions.json +++ b/api_docs/expressions.json @@ -33883,4 +33883,4 @@ } ] } -} \ No newline at end of file +} diff --git a/api_docs/lens.json b/api_docs/lens.json index 1c7581a8a1db6..235f2021e9823 100644 --- a/api_docs/lens.json +++ b/api_docs/lens.json @@ -330,7 +330,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", - "lineNumber": 72 + "lineNumber": 73 }, "signature": [ "Record boolean" @@ -542,7 +542,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/plugin.ts", - "lineNumber": 79 + "lineNumber": 81 }, "initialIsOpen": false }, @@ -1553,7 +1553,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", - "lineNumber": 75 + "lineNumber": 76 }, "signature": [ "{ columns: Record; columnOrder: string[]; incompleteColumns?: Record | undefined; }" diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index 7b771eb662616..8aac22c742433 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -2,42 +2,63 @@ [[advanced-queries]] === Query your data -Querying your APM data is a powerful tool that can make finding bottlenecks in your code even easier. -Imagine you have a user that complains about a slow response time in a specific service. -With the query bar, you can easily filter the APM app to only display trace data for that user, -or, to only show transactions that are slower than a specified time threshold. +Querying your APM data is an essential tool that can make finding bottlenecks in your code even more straightforward. -[float] -==== Example APM app queries +Using the query bar, a powerful data query feature, you can pass advanced queries on your data +to filter on specific pieces of information you’re interested in. + +The query bar comes with a handy autocomplete that helps find the fields and even provides suggestions to the data they include. +You can select the query bar and hit the down arrow on your keyboard to begin scanning recommendations. -* Exclude response times slower than 2000 ms: `transaction.duration.us > 2000000` -* Filter by response status code: `context.response.status_code ≥ 400` -* Filter by single user ID: `context.user.id : 12` +[float] +[[apm-app-advanced-queries]] +=== Querying in the APM app -When querying in the APM app, you're merely searching and selecting data from fields in Elasticsearch documents. -Queries entered into the query bar are also added as parameters to the URL, -so it's easy to share a specific query or view with others. +When querying in the APM app, you’re merely searching and selecting data from fields in {es} documents. Queries entered +into the query bar are also added as parameters to the URL, so it’s easy to share a specific query or view with others. When you type, you can begin to see some of the transaction fields available for filtering: [role="screenshot"] image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] -TIP: Read the {kibana-ref}/kuery-query.html[Kibana Query Language Enhancements] documentation to learn more about the capabilities of the {kib} query language. +[TIP] +===== +To learn more about the {kib} query language capabilities, see the {kibana-ref}/kuery-query.html[Kibana Query Language Enhancements] documentation. +===== + +[float] +[[apm-app-queries]] +==== APM app queries + +APM queries can be handy for removing noise from your data in the <>, <>, +<>, <>, and <> views. + +For example, in the *Services* view, you can quickly view a list of all the instrumented services running on your production +environment: `service.environment : production`. Or filter the list by including the APM agent's name and the host it’s running on: +`service.environment : "production" and agent.name : "java" and host.name : "prod-server1"`. + +On the *Traces* view, you might want to view failed transaction results from any of your running containers: +`transaction.result :"FAILURE" and container.id : *`. + +On the *Transactions* view, you may want to list only the slower transactions than a specified time threshold: `transaction.duration.us > 2000000`. +Or filter the list by including the service version and the Kubernetes pod it's running on: +`transaction.duration.us > 2000000 and service.version : "7.12.0" and kubernetes.pod.name : "pod-5468b47f57-pqk2m"`. [float] [[discover-advanced-queries]] === Querying in Discover Alternatively, you can query your APM documents in {kibana-ref}/discover.html[*Discover*]. -Querying documents in *Discover* works the same way as querying in the APM app, +Querying documents in *Discover* works the same way as queries in the APM app, and *Discover* supports all of the example APM app queries shown on this page. [float] -==== Example Discover query +[[discover-queries]] +==== Discover queries One example where you may want to make use of *Discover*, -is for viewing _all_ transactions for an endpoint, instead of just a sample. +is to view _all_ transactions for an endpoint instead of just a sample. TIP: Starting in v7.6, you can view ten samples per bucket in the APM app, instead of just one. diff --git a/docs/apm/correlations.asciidoc b/docs/apm/correlations.asciidoc index 1776cd72ac584..e184ca6bfa656 100644 --- a/docs/apm/correlations.asciidoc +++ b/docs/apm/correlations.asciidoc @@ -3,7 +3,8 @@ === Find latency and error correlations **Correlations** surface attributes of your data that are potentially correlated with high-latency or erroneous transactions. -Surfaced attributes are user-defined, meaning that they are completely customizable to your APM data. +By default, a number of attributes commonly known to cause performance issues, like version, +infrastructure, and location, are included, but all are completely customizable to your APM data. Find something interesting? A quick click of a button will auto-query your data as you work to resolve the underlying issue. For example, a site reliability engineer, who is responsible for keeping production systems up and running, @@ -11,8 +12,7 @@ notices an increase in latency in certain transactions. Analyzing metadata or tags that exist in high-latency transactions but not in lower-latency transactions can potentially point towards the root cause. They may find that a particular piece of hardware, like a host or pod, has failed, increasing latency. -Or, perhaps a set of users, based on IP address or region, is physically too far away from the nearest -data center, increasing latency. +Or, perhaps set of users, based on IP address or region, is facing increased latency due to local data center issues. [discrete] [[view-correlations]] @@ -27,8 +27,8 @@ Queries within the APM app apply to the correlations shown in the correlations f If a correlated field seems noteworthy, use the **Filter** quick links: -* `+` creates a new query in the APM app for transactions containing the selected value. -* `-` creates a new query in the APM app for transactions without the selected value. +* `+` creates a new query in the APM app for filtering transactions containing the selected value. +* `-` creates a new query in the APM app to filter out transactions containing the selected value. [discrete] [[correlations-latency]] @@ -37,8 +37,9 @@ If a correlated field seems noteworthy, use the **Filter** quick links: Correlations help you discover which fields are contributing to increased service latency. A latency distribution chart visualizes the overall latency of the selected service's transactions. -Correlated attributes are sorted by _Impact_–a visual representation of the score for the underlying -aggregation that powers correlations. +Correlated attributes are sorted by _Impact_–a visual representation of the +{ref}/search-aggregations-bucket-significantterms-aggregation.html[significant terms aggregation] +score that powers correlations. Attributes with a high impact, or attributes present in a large percentage of slow transactions, may contribute to increased latency. @@ -51,10 +52,15 @@ exists primarily in higher-latency transactions between 3.7 and 8.7 seconds. [role="screenshot"] image::apm/images/correlations-hover.png[Correlations hover effect] -Selecting the `+` filter creates a new query in the APM app for transactions with +Select the `+` filter to create a new query in the APM app for transactions with `user_agent.name: HeadlessChrome`. With the "noise" now filtered out, you can begin viewing sample traces to continue your investigation. +As you sift through high-latency transactions, you'll likely notice other interesting attributes. +Return to the correlations fly-out and select *Customize fields* to search on these new attributes. +You may need to do this a few times–each time filtering out more and more noise and bringing you +closer to a diagnosis. + [discrete] [[correlations-error-rate]] ==== Find error rate correlations @@ -62,8 +68,9 @@ you can begin viewing sample traces to continue your investigation. Correlations help you discover which fields are contributing to failed transactions. The Error rate over time chart visualizes the change in error rate over the selected time frame. -Correlated attributes are sorted by _Impact_–a visual representation of the score for the underlying -aggregation that powers correlations. +Correlated attributes are sorted by _Impact_–a visual representation of the +{ref}/search-aggregations-bucket-significantterms-aggregation.html[significant terms aggregation] +score that powers correlations. Attributes with a high impact, or attributes present in a large percentage of failed transactions, may contribute to increased error rates. @@ -76,16 +83,41 @@ existed in 100% of failed transactions between 6:00 and 10:30. [role="screenshot"] image::apm/images/error-rate-hover.png[Correlations errors hover effect] -Selecting the `+` filter creates a new query in the APM app for transactions with +Select the `+` filter to create a new query in the APM app for transactions with `url.original: http://localhost:3100...`. With the "noise" now filtered out, you can begin viewing sample traces to continue your investigation. +As you sift through erroneous transactions, you'll likely notice other interesting attributes. +Return to the correlations fly-out and select *Customize fields* to search on these new attributes. +You may need to do this a few times–each time filtering out more and more noise and bringing you +closer to a diagnosis. + [discrete] -[[correlations-custom-fields]] +[[correlations-customize-fields]] ==== Customize fields Correlations are only as good as the data they're searching for. -By default, a handful of potentially useful fields are selected, like `lables`, `service.version`, and `host.ip`. -You can remove and add fields to this list under the **Customize fields** dropdown. +By default, a handful of attributes commonly known to cause performance issues are included. +During the course of an investigation however, you may to need to add and remove fields from +this list multiple times as you narrow in on a diagnosis. + +Add and remove fields under the **Customize fields** dropdown. +The following fields are selected by default. +To keep the default list manageable, only the first six matching fields with wildcards are used. + +**Frontend (RUM) agent:** + +* `labels.*` +* `user.*` +* `user_agent.name` +* `user_agent.os.name` +* `url.original` + +**Backend agents:** + +* `labels.*` +* `host.ip` +* `service.node.name` +* `service.version` TIP: Want to start over? Select **reset** to clear your customizations. diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index 3fe9146658eef..56602ab7c05c9 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -6,49 +6,34 @@ Filter data ++++ -APM provides two different ways you can filter your data within the APM App: - -* <> -* <> - -[[global-filters]] -==== Global filters - -Global filters are ways you can filter any and all data across the APM app. -They are available in the Services, Transactions, Errors, Metrics, and Traces views, -and any filter applied will persist as you move between pages. +Global filters are ways you can filter data across the APM app based on a specific +time range or environment. They are available in the Services, Transactions, Errors, +Metrics, and Traces views, and any filter applied will persist as you move between pages. [role="screenshot"] image::apm/images/global-filters.png[Global filters available in the APM app in Kibana] -[float] -===== Global time range - -The <> in {kib} restricts APM data to a specific time period. - -[float] -[[query-bar]] -===== Query bar +[NOTE] +===== +If you prefer to use advanced queries on your data to filter on specific pieces +of information, see <>. +===== -The query bar is a powerful data query feature. -Similar to the query bar in {kibana-ref}/discover.html[Discover], -it enables you to pass advanced queries on your data to filter on particular pieces of information that you're interested in. -It comes with a handy autocomplete that helps find the fields and even provides suggestions to the data they include. -You can select the query bar and hit the down arrow on your keyboard to begin seeing recommendations. +[[global-time-range]] +==== Global time range -See <> for more information and sample queries. +The <> in {kib} restricts APM data to a specific time period. -[float] [[environment-selector]] -===== Service environment filter +==== Service environment filter The environment selector is a global filter for `service.environment`. -It allows you to view only relevant data, and is especially useful for separating development from production environments. +It allows you to view only relevant data and is especially useful for separating development from production environments. By default, all environments are displayed. If there are no environment options, you'll see "not defined". Service environments are defined when configuring your APM agents. It's vital to be consistent when naming environments in your agents. -See the documentation for each agent you're using to learn how to configure service environments: +To learn how to configure service environments, see the specific agent documentation: * *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] * *Java:* {apm-java-ref}/config-core.html#config-environment[`environment`] @@ -58,19 +43,3 @@ See the documentation for each agent you're using to learn how to configure serv * *Python:* {apm-py-ref}/configuration.html#config-environment[`environment`] * *Ruby:* {apm-ruby-ref}/configuration.html#config-environment[`environment`] * *Real User Monitoring:* {apm-rum-ref}/configuration.html#environment[`environment`] - -[[contextual-filters]] -==== Contextual filters - -Contextual filters are ways you can filter your specific APM data on each individual page. -The filters shown are relevant to your data, and will persist between pages, -but only where they are applicable -- they are typically most useful in their original context. -As an example, if you select a host on the Services overview, then select a transaction group, -the host filter will still be applied. - -These filters are very useful for quickly and easily removing noise from your data. -With just a click, you can filter your transactions by the transaction result, -host, container ID, Kubernetes pod, and more. - -[role="screenshot"] -image::apm/images/local-filter.png[Local filters available in the APM app in Kibana] \ No newline at end of file diff --git a/docs/apm/images/apm-errors-overview.png b/docs/apm/images/apm-errors-overview.png index 5b3b00a3b1ef2..425464a1ffd21 100644 Binary files a/docs/apm/images/apm-errors-overview.png and b/docs/apm/images/apm-errors-overview.png differ diff --git a/docs/apm/images/apm-metrics.png b/docs/apm/images/apm-metrics.png index af083b5ba3c08..c2d609c7c4cd5 100644 Binary files a/docs/apm/images/apm-metrics.png and b/docs/apm/images/apm-metrics.png differ diff --git a/docs/apm/images/apm-query-bar.png b/docs/apm/images/apm-query-bar.png index 92398065c2545..a1fb129d3c200 100644 Binary files a/docs/apm/images/apm-query-bar.png and b/docs/apm/images/apm-query-bar.png differ diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png index 3a56d597abfb7..1c16ac5b572c3 100644 Binary files a/docs/apm/images/apm-services-overview.png and b/docs/apm/images/apm-services-overview.png differ diff --git a/docs/apm/images/apm-traces.png b/docs/apm/images/apm-traces.png index ed15423b42c51..0e9062ee448b4 100644 Binary files a/docs/apm/images/apm-traces.png and b/docs/apm/images/apm-traces.png differ diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png index 1b25668f0fd92..be292c37e24e0 100644 Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ diff --git a/docs/apm/images/global-filters.png b/docs/apm/images/global-filters.png index 70ae50aea6057..f93a5214c316b 100644 Binary files a/docs/apm/images/global-filters.png and b/docs/apm/images/global-filters.png differ diff --git a/docs/apm/images/jvm-metrics-overview.png b/docs/apm/images/jvm-metrics-overview.png index 4b882574e2b9a..c6f28f7bdf48f 100644 Binary files a/docs/apm/images/jvm-metrics-overview.png and b/docs/apm/images/jvm-metrics-overview.png differ diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 5049321363f88..8cab7bb03da75 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -20,7 +20,7 @@ don't forget to check our other troubleshooting guides or discussion forum: * {apm-php-ref}/troubleshooting.html[PHP agent troubleshooting] * {apm-py-ref}/troubleshooting.html[Python agent troubleshooting] * {apm-ruby-ref}/debugging.html[Ruby agent troubleshooting] -* {apm-rum-ref/troubleshooting.html[RUM troubleshooting] +* {apm-rum-ref}/troubleshooting.html[RUM troubleshooting] * https://discuss.elastic.co/c/apm[APM discussion forum]. [discrete] diff --git a/docs/developer/telemetry.asciidoc b/docs/developer/telemetry.asciidoc index 75f42a860624b..45d2a140cf8b9 100644 --- a/docs/developer/telemetry.asciidoc +++ b/docs/developer/telemetry.asciidoc @@ -6,6 +6,7 @@ To help us provide a good developer experience, we track some straightforward me The operations we current report timing data for: * Total execution time of `yarn kbn bootstrap` +* Total execution time of `@kbn/optimizer` runs as well as the following metadata about the runs: The number of bundles created, the number of bundles which were cached, usage of `--watch`, `--dist`, `--workers` and `--no-cache` flags, and the count of themes being built. Along with the execution time of each execution, we ship the following information about your machine to the service: diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 026032a7b0740..df5ce62cc07af 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -99,8 +99,9 @@ readonly links: { readonly luceneExpressions: string; }; readonly indexPatterns: { - readonly loadingData: string; readonly introduction: string; + readonly fieldFormattersNumber: string; + readonly fieldFormattersString: string; }; readonly addData: string; readonly kibana: string; @@ -163,5 +164,6 @@ readonly links: { readonly ccs: Record; readonly plugins: Record; readonly snapshotRestore: Record; + readonly ingest: Record; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index d653623d5fe22..da3ae17171c81 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index b4faa4299a929..8dd4667002ead 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -300,7 +300,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttributeSingle](./kibana-plugin-core-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-server.savedobjectattribute.md) | | [SavedObjectMigrationFn](./kibana-plugin-core-server.savedobjectmigrationfn.md) | A migration function for a [saved object type](./kibana-plugin-core-server.savedobjectstype.md) used to migrate it to a given version | | [SavedObjectSanitizedDoc](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) | Describes Saved Object documents that have passed through the migration framework and are guaranteed to have a references root property. | -| [SavedObjectsClientContract](./kibana-plugin-core-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | +| [SavedObjectsClientContract](./kibana-plugin-core-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | | [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md). | | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md index 537cfbc175671..610356a733126 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md @@ -8,7 +8,7 @@ Saved Objects is Kibana's data persisentence mechanism allowing plugins to use E \#\# SavedObjectsClient errors -Since the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to application code. Ideally, all errors will be either: +Since the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either: 1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index 0f362f302104b..742b54e19216e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -9,6 +9,7 @@ ```typescript esFilters: { FilterLabel: (props: import("./ui/filter_bar/filter_editor/lib/filter_label").FilterLabelProps) => JSX.Element; + FilterItem: (props: import("./ui/filter_bar/filter_item").FilterItemProps) => JSX.Element; FILTERS: typeof FILTERS; FilterStateStore: typeof FilterStateStore; buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.submitonblur.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.submitonblur.md new file mode 100644 index 0000000000000..5188a951c149f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.submitonblur.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [submitOnBlur](./kibana-plugin-plugins-data-public.querystringinputprops.submitonblur.md) + +## QueryStringInputProps.submitOnBlur property + +Signature: + +```typescript +submitOnBlur?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.appendsessionstarttimetoname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.appendsessionstarttimetoname.md new file mode 100644 index 0000000000000..6b6b58d1838c9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.appendsessionstarttimetoname.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) > [appendSessionStartTimeToName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.appendsessionstarttimetoname.md) + +## SearchSessionInfoProvider.appendSessionStartTimeToName property + +Append session start time to a session name, `true` by default + +Signature: + +```typescript +appendSessionStartTimeToName?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md index 77125bc8deead..b6dfbd9fbb7cf 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md @@ -16,6 +16,7 @@ export interface SearchSessionInfoProviderboolean | Append session start time to a session name, true by default | | [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) | () => Promise<string> | User-facing name of the session. e.g. will be displayed in saved Search Sessions management list | | [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md index d408f00e33c9e..698b4bc7f2043 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md @@ -14,6 +14,6 @@ export declare class IndexPatternsServiceProvider implements PluginSignature: ```typescript -setup(core: CoreSetup, { expressions }: IndexPatternsServiceSetupDeps): void; +setup(core: CoreSetup, { logger, expressions }: IndexPatternsServiceSetupDeps): void; ``` ## Parameters @@ -15,7 +15,7 @@ setup(core: CoreSetup, { expressio | Parameter | Type | Description | | --- | --- | --- | | core | CoreSetup<DataPluginStartDependencies, DataPluginStart> | | -| { expressions } | IndexPatternsServiceSetupDeps | | +| { logger, expressions } | IndexPatternsServiceSetupDeps | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index e0734bc017f4f..16d9ce457603e 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -109,6 +109,5 @@ | [KibanaContext](./kibana-plugin-plugins-data-server.kibanacontext.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | | [Query](./kibana-plugin-plugins-data-server.query.md) | | -| [SearchRequestHandlerContext](./kibana-plugin-plugins-data-server.searchrequesthandlercontext.md) | | | [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchrequesthandlercontext.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchrequesthandlercontext.md deleted file mode 100644 index f031ddfbd09af..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchrequesthandlercontext.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchRequestHandlerContext](./kibana-plugin-plugins-data-server.searchrequesthandlercontext.md) - -## SearchRequestHandlerContext type - -Signature: - -```typescript -export declare type SearchRequestHandlerContext = IScopedSearchClient; -``` diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc index 02751ec57a1cf..e6b81d624b02e 100644 --- a/docs/glossary.asciidoc +++ b/docs/glossary.asciidoc @@ -13,9 +13,8 @@ + -- // tag::action-def[] -The rule-specific response that occurs when an alerting rule fires. -A rule can have multiple actions. -See +The rule-specific response that occurs when an alerting <> +fires. A rule can have multiple actions. See {kibana-ref}/action-types.html[Connectors and actions]. // end::action-def[] -- @@ -99,7 +98,8 @@ The cluster location is the weighted centroid for all documents in the grid cell [[glossary-condition]] condition :: // tag::condition-def[] -Specifies the circumstances that must be met to trigger an alerting rule. +Specifies the circumstances that must be met to trigger an alerting +<>. // end::condition-def[] [[glossary-connector]] connector :: diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 3ee7a0471eec1..5c27a7bdacdee 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -240,10 +240,6 @@ in the current index pattern is used. The columns that appear by default on the *Discover* page. The default is `_source`. -[[discover-aggs-terms-size]]`discover:aggs:terms:size`:: -The number terms that are visualized when clicking the *Visualize* button in the -field drop down. The default is `20`. - [[discover-samplesize]]`discover:sampleSize`:: The number of rows to show in the *Discover* table. diff --git a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc deleted file mode 100644 index d9745bfef524a..0000000000000 --- a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc +++ /dev/null @@ -1,170 +0,0 @@ -[role="xpack"] -[[ingest-node-pipelines]] -== Ingest Node Pipelines - -*Ingest Node Pipelines* enables you to create and manage {es} -pipelines that perform common transformations and -enrichments on your data. For example, you might remove a field, -rename an existing field, or set a new field. - -To begin, open the main menu, then click *Stack Management > Ingest Node Pipelines*. With *Ingest Node Pipelines*, you can: - -* View a list of your pipelines and drill down into details. -* Create a pipeline that defines a series of tasks, known as processors. -* Test a pipeline before feeding it with real data to ensure the pipeline works as expected. -* Delete a pipeline that is no longer needed. - -[role="screenshot"] -image:management/ingest-pipelines/images/ingest-pipeline-list.png["Ingest node pipeline list"] - -[float] -=== Required permissions - -The minimum required permissions to access *Ingest Node Pipelines* are -the `manage_pipeline` and `cluster:monitor/nodes/info` cluster privileges. - -To add privileges, open the main menu, then click *Stack Management > Roles*. - -[role="screenshot"] -image:management/ingest-pipelines/images/ingest-pipeline-privileges.png["Privileges required for Ingest Node Pipelines"] - -[float] -[[ingest-node-pipelines-manage]] -=== Manage pipelines - -From the list view, you can to drill down into the details of a pipeline. -To -edit, clone, or delete a pipeline, use the *Actions* menu. - -If you don’t have any pipelines, you can create one using the -*Create pipeline* form. You’ll define processors to transform documents -in a specific way. To handle exceptions, you can optionally define -failure processors to execute immediately after a failed processor. -Before creating the pipeline, you can verify it provides the expected output. - -[float] -[[ingest-node-pipelines-example]] -==== Example: Create a pipeline - -In this example, you’ll create a pipeline to handle server logs in the -Common Log Format. The log looks similar to this: - -[source,js] ----------------------------------- -212.87.37.154 - - [05/May/2020:16:21:15 +0000] \"GET /favicon.ico HTTP/1.1\" -200 3638 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) -AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36\" ----------------------------------- - -The log contains an IP address, timestamp, and user agent. You want to give -these three items their own field in {es} for fast search and visualization. -You also want to know where the request is coming from. - -. In *Ingest Node Pipelines*, click *Create a pipeline*. -. Provide a name and description for the pipeline. -. Add a grok processor to parse the log message: - -.. Click *Add a processor* and select the *Grok* processor type. -.. Set the field input to `message` and enter the following grok pattern: -+ -[source,js] ----------------------------------- -%{IPORHOST:clientip} %{USER:ident} %{USER:auth} \[%{HTTPDATE:timestamp}\] "%{WORD:verb} %{DATA:request} HTTP/%{NUMBER:httpversion}" %{NUMBER:response:int} (?:-|%{NUMBER:bytes:int}) %{QS:referrer} %{QS:agent} ----------------------------------- -+ -.. Click *Update* to save the processor. - -. Add processors to map the date, IP, and user agent fields. - -.. Map the appropriate field to each processor type: -+ --- -* **Date**: `timestamp` -* **GeoIP**: `clientip` -* **User agent**: `agent` - -For the **Date** processor, you also need to specify the date format you want to use: `dd/MMM/YYYY:HH:mm:ss Z`. --- -Your form should look similar to this: -+ -[role="screenshot"] -image:management/ingest-pipelines/images/ingest-pipeline-processor.png["Processors for Ingest Node Pipelines"] -+ -Alternatively, you can click the **Import processors** link and define the processors as JSON: -+ -[source,js] ----------------------------------- -{ - "processors": [ - { - "grok": { - "field": "message", - "patterns": ["%{IPORHOST:clientip} %{USER:ident} %{USER:auth} \\[%{HTTPDATE:timestamp}\\] \"%{WORD:verb} %{DATA:request} HTTP/%{NUMBER:httpversion}\" %{NUMBER:response:int} (?:-|%{NUMBER:bytes:int}) %{QS:referrer} %{QS:agent}"] - } - }, - { - "date": { - "field": "timestamp", - "formats": [ "dd/MMM/YYYY:HH:mm:ss Z" ] - } - }, - { - "geoip": { - "field": "clientip" - } - }, - { - "user_agent": { - "field": "agent" - } - } - ] -} ----------------------------------- -+ -The four {ref}/ingest-processors.html[processors] will run sequentially: -{ref}/grok-processor.html[grok], {ref}/date-processor.html[date], -{ref}/geoip-processor.html[geoip], and {ref}/user-agent-processor.html[user_agent]. You can reorder processors using the arrow icon next to each processor. - -. To test the pipeline to verify that it produces the expected results, click *Add documents*. - -. In the *Documents* tab, provide a sample document for testing: -+ -[source,js] ----------------------------------- -[ - { - "_source": { - "message": "212.87.37.154 - - [05/May/2020:16:21:15 +0000] \"GET /favicon.ico HTTP/1.1\" 200 3638 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36\"" - } - } -] ----------------------------------- - -. Click *Run the pipeline* and check if the pipeline worked as expected. -+ -You can also -view the verbose output and refresh the output from this view. - -. If everything looks correct, close the panel, and then click *Create pipeline*. -+ -At this point, you’re ready to use the Elasticsearch index API to load -the logs data. - -. In the Kibana Console, index a document with the pipeline -you created. -+ -[source,js] ----------------------------------- -PUT my-index/_doc/1?pipeline=access_logs -{ - "message": "212.87.37.154 - - [05/May/2020:16:21:15 +0000] \"GET /favicon.ico HTTP/1.1\" 200 3638 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36\"" -} ----------------------------------- - -. To verify, run: -+ -[source,js] ----------------------------------- -GET my-index/_doc/1 ----------------------------------- diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 935a433b39696..52d1d63ce0653 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -80,6 +80,156 @@ logging: ------------------- 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`. + +*Impact:* To restore the previous behavior, in `kibana.yml` use the `console` appender to send logs to `stdout`. +[source,yaml] +------------------- +logging: + root: + appenders: [default, console] +------------------- + +To send logs to `file` with a given file path, you should define a custom appender with `type:file`: +[source,yaml] +------------------- +logging: + appenders: + file: + type: file + fileName: /var/log/kibana.log + layout: + type: pattern + root: + appenders: [default, file] +------------------- + +[float] +==== Specify log event output with root +*Details:* Previously logging output would be specified by `logging.silent` (none), 'logging.quiet' (error messages only) and `logging.verbose` (all). + +*Impact:* To restore the previous behavior, in `kibana.yml` specify `logging.root.level` as one of `off`, `error`, `all`: +[source,yaml] +------------------- +# suppress all logs +logging: + root: + appenders: [default] + level: off +------------------- + +[source,yaml] +------------------- +# only log error messages +logging: + root: + appenders: [default] + level: error +------------------- + +[source,yaml] +------------------- +# log all events +logging: + root: + appenders: [default] + level: all +------------------- + +[float] +==== Suppress all log output with root +*Details:* Previously all logging output would be suppressed if `logging.silent` was true. + +*Impact:* To restore the previous behavior, in `kibana.yml` turn `logging.root.level` to 'off'. +[source,yaml] +------------------- +logging: + root: + appenders: [default] + level: off +------------------- + +[float] +==== Suppress log output with root +*Details:* Previously all logging output other than error messages would be suppressed if `logging.quiet` was true. + +*Impact:* To restore the previous behavior, in `kibana.yml` turn `logging.root.level` to 'error'. +[source,yaml] +------------------- +logging: + root: + appenders: [default] + level: error +------------------- + +[float] +==== Log all output with root +*Details:* Previously all events would be logged if `logging.verbose` was true. + +*Impact:* To restore the previous behavior, in `kibana.yml` turn `logging.root.level` to 'all'. +[source,yaml] +------------------- +logging: + root: + appenders: [default] + level: all +------------------- + +[float] +==== Declare log message format for each custom appender +*Details:* Previously all events would be logged in `json` format when `logging.json` was true. + +*Impact:* To restore the previous behavior, in `kibana.yml` configure the logging format for each custom appender with the `appender.layout` property. There is no default for custom appenders and each one must be configured expilictly. + +[source,yaml] +------------------- +logging: + appenders: + custom_console: + type: console + layout: + type: pattern + custom_json: + type: console + layout: + type: json + loggers: + - name: plugins.myPlugin + appenders: [custom_console] + root: + appenders: [default, custom_json] + level: warn +------------------- + +[float] +==== 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. + +[source,yaml] +------------------- +logging: + appenders: + rolling-file: + type: rolling-file + fileName: /var/logs/kibana.log + policy: + type: size-limit + size: 50mb + strategy: + type: numeric + pattern: '-%i' + max: 2 + layout: + type: pattern + loggers: + - name: plugins.myPlugin + appenders: [rolling-file] +------------------- + [float] ==== `xpack.security.authProviders` is no longer valid *Details:* The deprecated `xpack.security.authProviders` setting in the `kibana.yml` file has been removed. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index c7bdff800bb0b..a2ab1c10d9cd5 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -280,3 +280,8 @@ This content has moved. Refer to <>. [role="exclude",id="explore-dashboard-data"] This content has moved. Refer to <>. + +[role="exclude",id="ingest-node-pipelines"] +== Ingest Node Pipelines + +This content has moved. See {ref}/ingest.html[Ingest pipelines]. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index f29718e6d588b..5644cdbfc45ec 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -17,10 +17,9 @@ Consult your administrator if you do not have the appropriate access. [cols="50, 50"] |=== -| <> -| Create and manage {es} -pipelines that enable you to perform common transformations and -enrichments on your data. +| {ref}/ingest.html[Ingest Node Pipelines] +| Create and manage ingest pipelines that let you perform common transformations +and enrichments on your data. | {logstash-ref}/logstash-centralized-pipeline-management.html[Logstash Pipelines] | Create, edit, and delete your Logstash pipeline configurations. @@ -187,8 +186,6 @@ include::{kib-repo-dir}/management/alerting/connector-management.asciidoc[] include::{kib-repo-dir}/management/managing-beats.asciidoc[] -include::{kib-repo-dir}/management/ingest-pipelines/ingest-pipelines.asciidoc[] - include::{kib-repo-dir}/management/managing-fields.asciidoc[] include::{kib-repo-dir}/management/managing-licenses.asciidoc[] diff --git a/packages/kbn-config/src/env.ts b/packages/kbn-config/src/env.ts index 8526cfc7691ef..b6ff5e3b5aab2 100644 --- a/packages/kbn-config/src/env.ts +++ b/packages/kbn-config/src/env.ts @@ -22,8 +22,9 @@ export interface EnvOptions { export interface CliArgs { dev: boolean; envName?: string; - quiet: boolean; - silent: boolean; + /** @deprecated */ + quiet?: boolean; + silent?: boolean; watch: boolean; basePath: boolean; oss: boolean; diff --git a/packages/kbn-legacy-logging/src/schema.ts b/packages/kbn-legacy-logging/src/schema.ts index 2c5271157fb72..76d7381ee8728 100644 --- a/packages/kbn-legacy-logging/src/schema.ts +++ b/packages/kbn-legacy-logging/src/schema.ts @@ -11,7 +11,12 @@ import Joi from 'joi'; const HANDLED_IN_KIBANA_PLATFORM = Joi.any().description( 'This key is handled in the new platform ONLY' ); - +/** + * @deprecated + * + * Legacy logging has been deprecated and will be removed in 8.0. + * Set up logging from the platform logging instead + */ export interface LegacyLoggingConfig { silent: boolean; quiet: boolean; @@ -38,13 +43,11 @@ export const legacyLoggingConfigSchema = Joi.object() 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), }), - verbose: Joi.boolean().when('quiet', { is: true, then: Joi.valid(false).default(false), diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 8f82f34646e60..6e3106dbc2af7 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -18,6 +18,7 @@ import { logOptimizerState } from './log_optimizer_state'; import { OptimizerConfig } from './optimizer'; import { runOptimizer } from './run_optimizer'; import { validateLimitsForAllBundles, updateBundleLimits } from './limits'; +import { reportOptimizerTimings } from './report_optimizer_timings'; function getLimitsPath(flags: Flags, defaultPath: string) { if (flags.limits) { @@ -144,7 +145,9 @@ export function runKbnOptimizerCli(options: { defaultLimitsPath: string }) { const update$ = runOptimizer(config); - await lastValueFrom(update$.pipe(logOptimizerState(log, config))); + await lastValueFrom( + update$.pipe(logOptimizerState(log, config), reportOptimizerTimings(log, config)) + ); if (updateLimits) { updateBundleLimits({ diff --git a/packages/kbn-optimizer/src/common/theme_tags.test.ts b/packages/kbn-optimizer/src/common/theme_tags.test.ts index d0952a22da90d..126d1b1833873 100644 --- a/packages/kbn-optimizer/src/common/theme_tags.test.ts +++ b/packages/kbn-optimizer/src/common/theme_tags.test.ts @@ -11,8 +11,8 @@ import { parseThemeTags } from './theme_tags'; it('returns default tags when passed undefined', () => { expect(parseThemeTags()).toMatchInlineSnapshot(` Array [ - "v7dark", - "v7light", + "v8dark", + "v8light", ] `); }); diff --git a/packages/kbn-optimizer/src/common/theme_tags.ts b/packages/kbn-optimizer/src/common/theme_tags.ts index e889b5d3642c8..de95bbdcbcfea 100644 --- a/packages/kbn-optimizer/src/common/theme_tags.ts +++ b/packages/kbn-optimizer/src/common/theme_tags.ts @@ -17,7 +17,7 @@ const isArrayOfStrings = (input: unknown): input is string[] => export type ThemeTags = readonly ThemeTag[]; export type ThemeTag = 'v7light' | 'v7dark' | 'v8light' | 'v8dark'; -export const DEFAULT_THEMES = tags('v7light', 'v7dark'); +export const DEFAULT_THEMES = tags('v8light', 'v8dark'); export const ALL_THEMES = tags('v7light', 'v7dark', 'v8light', 'v8dark'); export function parseThemeTags(input?: any): ThemeTags { diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index 8d6e89008bc68..a5838a8a0fac8 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -12,3 +12,4 @@ export * from './log_optimizer_state'; export * from './node'; export * from './limits'; export * from './cli'; +export * from './report_optimizer_timings'; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 9e9e8960da21b..ffc8d7ea8c505 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -94,23 +94,23 @@ OptimizerConfig { "profileWebpack": false, "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "themeTags": Array [ - "v7dark", - "v7light", + "v8dark", + "v8light", ], "watch": false, } `; -exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=3)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/_other_styles.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/core/public/core_app/styles/_globals_v7dark.scss, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/core/public/core_app/styles/_globals_v7light.scss, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/core/public/core_app/styles/_globals_v8dark.scss, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/core/public/core_app/styles/_globals_v8light.scss, /packages/kbn-optimizer/target/worker/entry_point_creator.js, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts index 746064bfb3414..832fd812d36bb 100644 --- a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts @@ -94,8 +94,8 @@ describe('getOptimizerCacheKey()', () => { "optimizerCacheKey": "♻", "repoRoot": , "themeTags": Array [ - "v7dark", - "v7light", + "v8dark", + "v8light", ], }, } diff --git a/packages/kbn-optimizer/src/report_optimizer_timings.ts b/packages/kbn-optimizer/src/report_optimizer_timings.ts new file mode 100644 index 0000000000000..dcb3a0fba77b5 --- /dev/null +++ b/packages/kbn-optimizer/src/report_optimizer_timings.ts @@ -0,0 +1,73 @@ +/* + * 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 { concatMap } from 'rxjs/operators'; +import { CiStatsReporter, ToolingLog } from '@kbn/dev-utils'; + +import { OptimizerConfig } from './optimizer'; +import { OptimizerUpdate$ } from './run_optimizer'; +import { pipeClosure } from './common'; + +export function reportOptimizerTimings(log: ToolingLog, config: OptimizerConfig) { + return pipeClosure((update$: OptimizerUpdate$) => { + let sent = false; + + const cachedBundles = new Set(); + const notCachedBundles = new Set(); + + return update$.pipe( + concatMap(async (update) => { + // if we've already sent timing data then move on + if (sent) { + return update; + } + + if (update.event?.type === 'bundle cached') { + cachedBundles.add(update.event.bundle.id); + } + if (update.event?.type === 'bundle not cached') { + notCachedBundles.add(update.event.bundle.id); + } + + // wait for the optimizer to complete, either with a success or failure + if (update.state.phase !== 'issue' && update.state.phase !== 'success') { + return update; + } + + sent = true; + const reporter = CiStatsReporter.fromEnv(log); + const time = Date.now() - update.state.startTime; + + await reporter.timings({ + timings: [ + { + group: '@kbn/optimizer', + id: 'overall time', + ms: time, + meta: { + optimizerBundleCount: config.bundles.length, + optimizerBundleCacheCount: cachedBundles.size, + optimizerBundleCachePct: Math.floor( + (cachedBundles.size / config.bundles.length) * 100 + ), + optimizerWatch: config.watch, + optimizerProduction: config.dist, + optimizerProfileWebpack: config.profileWebpack, + optimizerBundleThemeTagsCount: config.themeTags.length, + optimizerCache: config.cache, + optimizerMaxWorkerCount: config.maxWorkerCount, + }, + }, + ], + }); + + return update; + }) + ); + }); +} diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index ffe31396422c8..a8e4ffd295aec 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -270,7 +270,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: extractComments: false, parallel: false, terserOptions: { - compress: false, + compress: true, mangle: false, }, }), diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/all_extracted_collectors.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/all_extracted_collectors.ts index 37bdd327f945b..1f74a2a02eb1e 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/all_extracted_collectors.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/all_extracted_collectors.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { ParsedUsageCollection } from '../ts_parser'; import { parsedExternallyDefinedCollector } from './parsed_externally_defined_collector'; import { parsedImportedSchemaCollector } from './parsed_imported_schema'; import { parsedImportedUsageInterface } from './parsed_imported_usage_interface'; @@ -14,15 +15,18 @@ import { parsedNestedCollector } from './parsed_nested_collector'; import { parsedSchemaDefinedWithSpreadsCollector } from './parsed_schema_defined_with_spreads_collector'; import { parsedWorkingCollector } from './parsed_working_collector'; import { parsedCollectorWithDescription } from './parsed_working_collector_with_description'; -import { ParsedUsageCollection } from '../ts_parser'; +import { parsedStatsCollector } from './parsed_stats_collector'; +import { parsedImportedInterfaceFromExport } from './parsed_imported_interface_from_export'; export const allExtractedCollectors: ParsedUsageCollection[] = [ ...parsedExternallyDefinedCollector, + ...parsedImportedInterfaceFromExport, ...parsedImportedSchemaCollector, ...parsedImportedUsageInterface, parsedIndexedInterfaceWithNoMatchingSchema, parsedNestedCollector, parsedSchemaDefinedWithSpreadsCollector, + ...parsedStatsCollector, parsedCollectorWithDescription, parsedWorkingCollector, ]; diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_interface_from_export.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_interface_from_export.ts new file mode 100644 index 0000000000000..42f958d1e33c5 --- /dev/null +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_interface_from_export.ts @@ -0,0 +1,35 @@ +/* + * 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 { SyntaxKind } from 'typescript'; +import { ParsedUsageCollection } from '../ts_parser'; + +export const parsedImportedInterfaceFromExport: ParsedUsageCollection[] = [ + [ + 'src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts', + { + collectorName: 'importing_from_export_collector', + schema: { + value: { + some_field: { + type: 'keyword', + }, + }, + }, + fetch: { + typeName: 'Usage', + typeDescriptor: { + some_field: { + kind: SyntaxKind.StringKeyword, + type: 'StringKeyword', + }, + }, + }, + }, + ], +]; diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_stats_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_stats_collector.ts new file mode 100644 index 0000000000000..828372bf0b7d9 --- /dev/null +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_stats_collector.ts @@ -0,0 +1,35 @@ +/* + * 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 { SyntaxKind } from 'typescript'; +import { ParsedUsageCollection } from '../ts_parser'; + +export const parsedStatsCollector: ParsedUsageCollection[] = [ + [ + 'src/fixtures/telemetry_collectors/stats_collector.ts', + { + collectorName: 'my_stats_collector_with_schema', + schema: { + value: { + some_field: { + type: 'keyword', + }, + }, + }, + fetch: { + typeName: 'Usage', + typeDescriptor: { + some_field: { + kind: SyntaxKind.StringKeyword, + type: 'StringKeyword', + }, + }, + }, + }, + ], +]; diff --git a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts index 5106ac7855fc6..5eee06a5182ee 100644 --- a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts @@ -24,7 +24,7 @@ describe('extractCollectors', () => { const programPaths = await getProgramPaths(configs[0]); const results = [...extractCollectors(programPaths, tsConfig)]; - expect(results).toHaveLength(9); + expect(results).toHaveLength(11); expect(results).toStrictEqual(allExtractedCollectors); }); }); diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.ts b/packages/kbn-telemetry-tools/src/tools/serializer.ts index b3111af5eec94..9bde3cb839364 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.ts @@ -202,7 +202,7 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | return getDescriptor(node.typeName, program); } - if (ts.isImportSpecifier(node)) { + if (ts.isImportSpecifier(node) || ts.isExportSpecifier(node)) { const source = node.getSourceFile(); const importedModuleName = getModuleSpecifier(node); diff --git a/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts b/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts index 761645b9887da..4a58e3fc1101b 100644 --- a/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts @@ -15,6 +15,8 @@ import { parsedExternallyDefinedCollector } from './__fixture__/parsed_externall import { parsedImportedUsageInterface } from './__fixture__/parsed_imported_usage_interface'; import { parsedImportedSchemaCollector } from './__fixture__/parsed_imported_schema'; import { parsedSchemaDefinedWithSpreadsCollector } from './__fixture__/parsed_schema_defined_with_spreads_collector'; +import { parsedStatsCollector } from './__fixture__/parsed_stats_collector'; +import { parsedImportedInterfaceFromExport } from './__fixture__/parsed_imported_interface_from_export'; export function loadFixtureProgram(fixtureName: string) { const fixturePath = path.resolve( @@ -89,6 +91,18 @@ describe('parseUsageCollection', () => { expect(result).toEqual(parsedImportedUsageInterface); }); + it('parses stats collectors, discarding those without schemas', () => { + const { program, sourceFile } = loadFixtureProgram('stats_collector.ts'); + const result = [...parseUsageCollection(sourceFile, program)]; + expect(result).toEqual(parsedStatsCollector); + }); + + it('follows `export { Usage } from "./path"` expressions', () => { + const { program, sourceFile } = loadFixtureProgram('imported_interface_from_export/index.ts'); + const result = [...parseUsageCollection(sourceFile, program)]; + expect(result).toEqual(parsedImportedInterfaceFromExport); + }); + it('skips files that do not define a collector', () => { const { program, sourceFile } = loadFixtureProgram('file_with_no_collector.ts'); const result = [...parseUsageCollection(sourceFile, program)]; diff --git a/packages/kbn-telemetry-tools/src/tools/ts_parser.ts b/packages/kbn-telemetry-tools/src/tools/ts_parser.ts index 1e2bb0a0dbed0..9431e7e053684 100644 --- a/packages/kbn-telemetry-tools/src/tools/ts_parser.ts +++ b/packages/kbn-telemetry-tools/src/tools/ts_parser.ts @@ -41,6 +41,24 @@ export function isMakeUsageCollectorFunction( return false; } +export function isMakeStatsCollectorFunctionWithSchema( + node: ts.Node, + sourceFile: ts.SourceFile +): node is ts.CallExpression { + if (ts.isCallExpression(node)) { + const isMakeStatsCollector = /makeStatsCollector$/.test(node.expression.getText(sourceFile)); + if (isMakeStatsCollector) { + const collectorConfig = getCollectionConfigNode(node, sourceFile); + const schemaProperty = getProperty(collectorConfig, 'schema'); + if (schemaProperty) { + return true; + } + } + } + + return false; +} + export interface CollectorDetails { collectorName: string; fetch: { typeName: string; typeDescriptor: Descriptor }; @@ -140,6 +158,7 @@ function extractCollectorDetails( throw Error(`usageCollector.schema must be be an object.`); } + // TODO: Try to infer the output type from fetch instead of being explicit const collectorNodeType = collectorNode.typeArguments; if (!collectorNodeType || collectorNodeType?.length === 0) { throw Error(`makeUsageCollector requires a Usage type makeUsageCollector({ ... }).`); @@ -172,7 +191,19 @@ export function sourceHasUsageCollector(sourceFile: ts.SourceFile) { } return false; - return true; +} + +export function sourceHasStatsCollector(sourceFile: ts.SourceFile) { + if (sourceFile.isDeclarationFile === true || (sourceFile as any).identifierCount === 0) { + return false; + } + + const identifiers = (sourceFile as any).identifiers; + if (identifiers.get('makeStatsCollector')) { + return true; + } + + return false; } export type ParsedUsageCollection = [string, CollectorDetails]; @@ -182,9 +213,12 @@ export function* parseUsageCollection( program: ts.Program ): Generator { const relativePath = path.relative(process.cwd(), sourceFile.fileName); - if (sourceHasUsageCollector(sourceFile)) { + if (sourceHasUsageCollector(sourceFile) || sourceHasStatsCollector(sourceFile)) { for (const node of traverseNodes(sourceFile)) { - if (isMakeUsageCollectorFunction(node, sourceFile)) { + if ( + isMakeUsageCollectorFunction(node, sourceFile) || + isMakeStatsCollectorFunctionWithSchema(node, sourceFile) + ) { try { const collectorDetails = extractCollectorDetails(node, program, sourceFile); yield [relativePath, collectorDetails]; diff --git a/packages/kbn-telemetry-tools/src/tools/utils.ts b/packages/kbn-telemetry-tools/src/tools/utils.ts index 52362668c2f53..c9526fe7d0403 100644 --- a/packages/kbn-telemetry-tools/src/tools/utils.ts +++ b/packages/kbn-telemetry-tools/src/tools/utils.ts @@ -65,7 +65,9 @@ export function getIdentifierDeclarationFromSource(node: ts.Node, source: ts.Sou } const identifierName = node.getText(); - const identifierDefinition: ts.Node = (source as any).locals.get(identifierName); + const identifierDefinition: ts.Node = + (source as any).locals.get(identifierName) || + (source as any).symbol.exports.get(identifierName); if (!identifierDefinition) { throw new Error(`Unable to find identifier in source ${identifierName}`); } diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 891f2b0fff797..13c16691bf12a 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -51,7 +51,6 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { const get = _.partial(_.get, rawConfig); const has = _.partial(_.has, rawConfig); const merge = _.partial(_.merge, rawConfig); - if (opts.oss) { delete rawConfig.xpack; } @@ -112,10 +111,18 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { if (opts.elasticsearch) set('elasticsearch.hosts', opts.elasticsearch.split(',')); if (opts.port) set('server.port', opts.port); if (opts.host) set('server.host', opts.host); - if (opts.quiet) set('logging.quiet', true); - if (opts.silent) set('logging.silent', true); - if (opts.verbose) set('logging.verbose', true); - if (opts.logFile) set('logging.dest', opts.logFile); + if (opts.silent) { + set('logging.silent', true); + set('logging.root.level', 'off'); + } + if (opts.verbose) { + if (has('logging.root.appenders')) { + set('logging.root.level', 'all'); + } else { + // Only set logging.verbose to true for legacy logging when KP logging isn't configured. + set('logging.verbose', true); + } + } set('plugins.scanDirs', _.compact([].concat(get('plugins.scanDirs'), opts.pluginDir))); set('plugins.paths', _.compact([].concat(get('plugins.paths'), opts.pluginPath))); @@ -140,11 +147,14 @@ export default function (program) { [getConfigPath()] ) .option('-p, --port ', 'The port to bind to', parseInt) - .option('-q, --quiet', 'Prevent all logging except errors') + .option('-q, --quiet', 'Deprecated, set logging level in your configuration') .option('-Q, --silent', 'Prevent all logging') .option('--verbose', 'Turns on verbose logging') .option('-H, --host ', 'The host to bind to') - .option('-l, --log-file ', 'The file to log to') + .option( + '-l, --log-file ', + 'Deprecated, set logging file destination in your configuration' + ) .option( '--plugin-dir ', 'A path to scan for plugins, this can be specified multiple ' + @@ -204,6 +214,7 @@ export default function (program) { cliArgs: { dev: !!opts.dev, envName: unknownOptions.env ? unknownOptions.env.name : undefined, + // no longer supported quiet: !!opts.quiet, silent: !!opts.silent, watch: !!opts.watch, diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 759253aec80e5..e187b7ea581bf 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -16,6 +16,7 @@ interface StartDeps { /** @internal */ export class DocLinksService { public setup() {} + public start({ injectedMetadata }: StartDeps): DocLinksStart { const DOC_LINK_VERSION = injectedMetadata.getKibanaBranch(); const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/'; @@ -110,7 +111,7 @@ export class DocLinksService { runtimeFields: `${ELASTICSEARCH_DOCS}runtime.html`, scriptedFields: { scriptFields: `${ELASTICSEARCH_DOCS}search-request-script-fields.html`, - scriptAggs: `${ELASTICSEARCH_DOCS}search-aggregations.html#_values_source`, + scriptAggs: `${ELASTICSEARCH_DOCS}search-aggregations.html`, painless: `${ELASTICSEARCH_DOCS}modules-scripting-painless.html`, painlessApi: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-api-reference.html`, painlessLangSpec: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-lang-spec.html`, @@ -120,8 +121,8 @@ export class DocLinksService { luceneExpressions: `${ELASTICSEARCH_DOCS}modules-scripting-expression.html`, }, indexPatterns: { - loadingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/tutorial-load-dataset.html`, introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, + fieldFormattersNumber: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/numeral.html`, fieldFormattersString: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/field-formatters-string.html`, }, addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, @@ -201,10 +202,10 @@ export class DocLinksService { emailActionConfig: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/email-action-type.html#configuring-email`, generalSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html#general-alert-action-settings`, indexAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html`, - esQuery: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-type-es-query.html`, - indexThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-type-index-threshold.html#index-action-configuration`, + esQuery: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/rule-type-es-query.html`, + indexThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/rule-type-index-threshold.html`, pagerDutyAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pagerduty-action-type.html`, - preconfiguredConnectors: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pre-configured-action-types-and-connectors.html`, + preconfiguredConnectors: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pre-configured-connectors.html`, serviceNowAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/servicenow-action-type.html#configuring-servicenow`, setupPrerequisites: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`, slackAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/slack-action-type.html#configuring-slack`, @@ -284,6 +285,11 @@ export class DocLinksService { registerUrl: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-read-only-repository`, restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`, }, + ingest: { + pipelines: `${ELASTICSEARCH_DOCS}ingest.html`, + pipelineFailure: `${ELASTICSEARCH_DOCS}ingest.html#handling-pipeline-failures`, + processors: `${ELASTICSEARCH_DOCS}processors.html`, + }, }, }); } @@ -385,8 +391,9 @@ export interface DocLinksStart { readonly luceneExpressions: string; }; readonly indexPatterns: { - readonly loadingData: string; readonly introduction: string; + readonly fieldFormattersNumber: string; + readonly fieldFormattersString: string; }; readonly addData: string; readonly kibana: string; @@ -449,5 +456,6 @@ export interface DocLinksStart { readonly ccs: Record; readonly plugins: Record; readonly snapshotRestore: Record; + readonly ingest: Record; }; } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d79cba5346a73..396bf16cbdc6f 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -570,8 +570,9 @@ export interface DocLinksStart { readonly luceneExpressions: string; }; readonly indexPatterns: { - readonly loadingData: string; readonly introduction: string; + readonly fieldFormattersNumber: string; + readonly fieldFormattersString: string; }; readonly addData: string; readonly kibana: string; @@ -634,6 +635,7 @@ export interface DocLinksStart { readonly ccs: Record; readonly plugins: Record; readonly snapshotRestore: Record; + readonly ingest: Record; }; } diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index 4d7dafd2162c2..b6b3ab5b8face 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -244,17 +244,10 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.events.ops\\" has been deprecated and will be removed in 8.0. To access ops data moving forward, please enable debug logs for the \\"metrics.ops\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md", + "\\"logging.events.ops\\" has been deprecated and will be removed in 8.0. To access ops data moving forward, please enable debug logs for the \\"metrics.ops\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", ] `); }); - - it('does not warn when other events are configured', () => { - const { messages } = applyCoreDeprecations({ - logging: { events: { log: '*' } }, - }); - expect(messages).toEqual([]); - }); }); describe('logging.events.request and logging.events.response', () => { @@ -264,7 +257,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md", + "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", ] `); }); @@ -275,7 +268,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md", + "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", ] `); }); @@ -286,36 +279,187 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md", + "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", ] `); }); + }); - it('does not warn when other events are configured', () => { + describe('logging.timezone', () => { + it('warns when ops events are used', () => { const { messages } = applyCoreDeprecations({ - logging: { events: { log: '*' } }, + logging: { timezone: 'GMT' }, }); - expect(messages).toEqual([]); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.timezone\\" has been deprecated and will be removed in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", + ] + `); }); }); - describe('logging.timezone', () => { - it('warns when ops events are used', () => { + describe('logging.dest', () => { + it('warns when dest is used', () => { const { messages } = applyCoreDeprecations({ - logging: { timezone: 'GMT' }, + logging: { dest: 'stdout' }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.", + ] + `); + }); + it('warns when dest path is given', () => { + const { messages } = applyCoreDeprecations({ + logging: { dest: '/log-log.txt' }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.", + ] + `); + }); + }); + + describe('logging.quiet, logging.silent and logging.verbose', () => { + it('warns when quiet is used', () => { + const { messages } = applyCoreDeprecations({ + logging: { quiet: true }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.quiet\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:error\\" in your logging configuration. ", + ] + `); + }); + it('warns when silent is used', () => { + const { messages } = applyCoreDeprecations({ + logging: { silent: true }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.silent\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:off\\" in your logging configuration. ", + ] + `); + }); + it('warns when verbose is used', () => { + const { messages } = applyCoreDeprecations({ + logging: { verbose: true }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.verbose\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:all\\" in your logging configuration. ", + ] + `); + }); + }); + + describe('logging.json', () => { + it('warns when json is used', () => { + const { messages } = applyCoreDeprecations({ + logging: { json: true }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.json\\" has been deprecated and will be removed in 8.0. To specify log message format moving forward, you can configure the \\"appender.layout\\" property for every custom appender in your logging configuration. There is currently no default layout for custom appenders and each one must be declared explicitly. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.", + ] + `); + }); + }); + + describe('logging.rotate.enabled, logging.rotate.usePolling, logging.rotate.pollingInterval, logging.rotate.everyBytes and logging.rotate.keepFiles', () => { + it('warns when logging.rotate configurations are used', () => { + const { messages } = applyCoreDeprecations({ + logging: { rotate: { enabled: true } }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender", + ] + `); + }); + + it('warns when logging.rotate polling configurations are used', () => { + const { messages } = applyCoreDeprecations({ + logging: { rotate: { enabled: true, usePolling: true, pollingInterval: 5000 } }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender", + ] + `); + }); + + it('warns when logging.rotate.everyBytes configurations are used', () => { + const { messages } = applyCoreDeprecations({ + logging: { rotate: { enabled: true, everyBytes: 1048576 } }, }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.timezone\\" has been deprecated and will be removed in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md", + "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender", ] `); }); - it('does not warn when other events are configured', () => { + it('warns when logging.rotate.keepFiles is used', () => { const { messages } = applyCoreDeprecations({ - logging: { events: { log: '*' } }, + logging: { rotate: { enabled: true, keepFiles: 1024 } }, }); - expect(messages).toEqual([]); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender", + ] + `); + }); + }); + + describe('logging.events.log', () => { + it('warns when events.log is used', () => { + const { messages } = applyCoreDeprecations({ + logging: { events: { log: ['info'] } }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.events.log\\" has been deprecated and will be removed in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration. ", + ] + `); + }); + }); + + describe('logging.events.error', () => { + it('warns when events.error is used', () => { + const { messages } = applyCoreDeprecations({ + logging: { events: { error: ['some error'] } }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.events.error\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level: error\\" in your logging configuration. ", + ] + `); + }); + }); + + describe('logging.filter', () => { + it('warns when filter.cookie is used', () => { + const { messages } = applyCoreDeprecations({ + logging: { filter: { cookie: 'none' } }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.filter\\" has been deprecated and will be removed in 8.0. ", + ] + `); + }); + + it('warns when filter.authorization is used', () => { + const { messages } = applyCoreDeprecations({ + logging: { filter: { authorization: 'none' } }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.filter\\" has been deprecated and will be removed in 8.0. ", + ] + `); }); }); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index fbdbaeb14fd59..565b957b2a8e1 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -109,7 +109,7 @@ const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, log) '"logging.events.ops" has been deprecated and will be removed ' + 'in 8.0. To access ops data moving forward, please enable debug logs for the ' + '"metrics.ops" context in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md' + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx' ); } return settings; @@ -121,7 +121,7 @@ const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, l '"logging.events.request" and "logging.events.response" have been deprecated and will be removed ' + 'in 8.0. To access request and/or response data moving forward, please enable debug logs for the ' + '"http.server.response" context in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md' + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx' ); } return settings; @@ -133,12 +133,111 @@ const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) '"logging.timezone" has been deprecated and will be removed ' + 'in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern ' + 'in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md' + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx' ); } return settings; }; +const destLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.dest')) { + log( + '"logging.dest" has been deprecated and will be removed ' + + 'in 8.0. To set the destination moving forward, you can use the "console" appender ' + + 'in your logging configuration or define a custom one. For more details, see ' + + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.' + ); + } + return settings; +}; + +const quietLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.quiet')) { + log( + '"logging.quiet" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level:error" in your logging configuration. ' + ); + } + return settings; +}; + +const silentLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.silent')) { + log( + '"logging.silent" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level:off" in your logging configuration. ' + ); + } + return settings; +}; + +const verboseLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.verbose')) { + log( + '"logging.verbose" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level:all" in your logging configuration. ' + ); + } + return settings; +}; + +const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + // We silence the deprecation warning when running in development mode because + // the dev CLI code in src/dev/cli_dev_mode/using_server_process.ts manually + // specifies `--logging.json=false`. Since it's executed in a child process, the + // ` legacyLoggingConfigSchema` returns `true` for the TTY check on `process.stdout.isTTY` + if (has(settings, 'logging.json') && settings.env !== 'development') { + log( + '"logging.json" has been deprecated and will be removed ' + + 'in 8.0. To specify log message format moving forward, ' + + 'you can configure the "appender.layout" property for every custom appender in your logging configuration. ' + + 'There is currently no default layout for custom appenders and each one must be declared explicitly. ' + + 'For more details, see ' + + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.' + ); + } + return settings; +}; + +const logRotateDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.rotate')) { + log( + '"logging.rotate" and sub-options have been deprecated and will be removed in 8.0. ' + + 'Moving forward, you can enable log rotation using the "rolling-file" appender for a logger ' + + 'in your logging configuration. For more details, see ' + + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender' + ); + } + return settings; +}; + +const logEventsLogDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.events.log')) { + log( + '"logging.events.log" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration. ' + ); + } + return settings; +}; + +const logEventsErrorDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.events.error')) { + log( + '"logging.events.error" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level: error" in your logging configuration. ' + ); + } + return settings; +}; + +const logFilterDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.filter')) { + log('"logging.filter" has been deprecated and will be removed ' + 'in 8.0. '); + } + return settings; +}; + export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [ unusedFromRoot('savedObjects.indexCheckTimeout'), unusedFromRoot('server.xsrf.token'), @@ -176,4 +275,13 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unu opsLoggingEventDeprecation, requestLoggingEventDeprecation, timezoneLoggingDeprecation, + destLoggingDeprecation, + quietLoggingDeprecation, + silentLoggingDeprecation, + verboseLoggingDeprecation, + jsonLoggingDeprecation, + logRotateDeprecation, + logEventsLogDeprecation, + logEventsErrorDeprecation, + logFilterDeprecation, ]; diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts index 4cb0ec02d85f7..5b672774c515a 100644 --- a/src/core/server/config/integration_tests/config_deprecation.test.ts +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -23,13 +23,17 @@ describe('configuration deprecations', () => { } }); - it('should not log deprecation warnings for default configuration', async () => { + it('should not log deprecation warnings for default configuration that is not one of `logging.verbose`, `logging.quiet` or `logging.silent`', async () => { root = kbnTestServer.createRoot(); await root.setup(); const logs = loggingSystemMock.collect(mockLoggingSystem); - expect(logs.warn.flat()).toMatchInlineSnapshot(`Array []`); + expect(logs.warn.flat()).toMatchInlineSnapshot(` + Array [ + "\\"logging.silent\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:off\\" in your logging configuration. ", + ] + `); }); it('should log deprecation warnings for core deprecations', async () => { @@ -47,6 +51,7 @@ describe('configuration deprecations', () => { Array [ "optimize.lazy is deprecated and is no longer used", "optimize.lazyPort is deprecated and is no longer used", + "\\"logging.silent\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:off\\" in your logging configuration. ", ] `); }); diff --git a/src/core/server/logging/README.mdx b/src/core/server/logging/README.mdx index 63a7d8ecade6c..08e4ed34204c0 100644 --- a/src/core/server/logging/README.mdx +++ b/src/core/server/logging/README.mdx @@ -563,9 +563,9 @@ The log will be less verbose with `warn` level for the `server` context name: ### Logging config migration Compatibility with the legacy logging system is assured until the end of the `v7` version. -All log messages handled by `root` context are forwarded to the legacy logging service. If you re-write +All log messages handled by `root` context are forwarded to the legacy logging service using a `default` appender. If you re-write root appenders, make sure that it contains `default` appender to provide backward compatibility. -**Note**: If you define an appender for a context name, the log messages aren't handled by the +**Note**: If you define an appender for a context name, the log messages for that specific context aren't handled by the `root` context anymore and not forwarded to the legacy logging service. #### logging.dest @@ -659,6 +659,23 @@ and you can enable them by adjusting the minimum required [logging level](#log-l #### logging.filter TBD +#### logging.rotate +Specify the options for the logging rotate feature and only applicable when logs are written to file. +With the new logging config, the log rotation feature is provided by the `rolling-file` [appender](#rolling-file-appender). + +**`logging.rotate.enabled` and `logging.rotate.usePolling`** +Enables log rotation when `enabled` is set to `true`. The `usePolling` option is optional and can be used in systems where the watch api is not accurate. +With the new logging config log rotation is provided by the rolling file appender. Polling will apply by default when the `rolling-file` appender is configured. + +**`logging.rotate.pollingInterval`** +The number of milliseconds for the polling strategy in the case when `logging.rotate.usePolling` is enabled and defaults to 10000. +Possible range from 5000 to 3600000. With the new logging config an time interval can be configured with the +[TimeIntervalTriggeringPolicy](#timeintervaltriggeringpolicy) + +**`logging.rotate.everyBytes` and `logging.rotate.keepFiles`** +Maximum size of a log file and the number of most recent log files to keep on disk. With the new logging config the log size limit can be configured with the +[SizeLimitTriggeringPolicy](#sizelimitriggeringpolicy) and the number of files to keep with the `numeric` strategy `max` option. + ### Logging configuration via CLI | legacy logging | Kibana Platform logging| diff --git a/src/core/server/rendering/bootstrap/bootstrap_renderer.test.ts b/src/core/server/rendering/bootstrap/bootstrap_renderer.test.ts index 3803d38a968c1..36551def5eef0 100644 --- a/src/core/server/rendering/bootstrap/bootstrap_renderer.test.ts +++ b/src/core/server/rendering/bootstrap/bootstrap_renderer.test.ts @@ -180,7 +180,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v7', + themeVersion: 'v8', darkMode: false, }); }); diff --git a/src/core/server/rendering/bootstrap/bootstrap_renderer.ts b/src/core/server/rendering/bootstrap/bootstrap_renderer.ts index cff593e5c5aa9..edc0f4f0a2203 100644 --- a/src/core/server/rendering/bootstrap/bootstrap_renderer.ts +++ b/src/core/server/rendering/bootstrap/bootstrap_renderer.ts @@ -50,12 +50,12 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({ return async function bootstrapRenderer({ uiSettingsClient, request }) { let darkMode = false; - let themeVersion = 'v7'; + let themeVersion = 'v8'; try { const authenticated = isAuthenticated(request); darkMode = authenticated ? await uiSettingsClient.get('theme:darkMode') : false; - themeVersion = authenticated ? await uiSettingsClient.get('theme:version') : 'v7'; + themeVersion = authenticated ? await uiSettingsClient.get('theme:version') : 'v8'; } catch (e) { // just use the default values in case of connectivity issues with ES } diff --git a/src/core/server/ui_settings/settings/theme.test.ts b/src/core/server/ui_settings/settings/theme.test.ts index 5c66712b6a4ba..f0ca4f1eff4cd 100644 --- a/src/core/server/ui_settings/settings/theme.test.ts +++ b/src/core/server/ui_settings/settings/theme.test.ts @@ -35,11 +35,11 @@ describe('theme settings', () => { it('should only accept valid values', () => { expect(() => validate('v7')).not.toThrow(); - expect(() => validate('v8 (beta)')).not.toThrow(); + expect(() => validate('v8')).not.toThrow(); expect(() => validate('v12')).toThrowErrorMatchingInlineSnapshot(` "types that failed validation: - [0]: expected value to equal [v7] -- [1]: expected value to equal [v8 (beta)]" +- [1]: expected value to equal [v8]" `); }); }); diff --git a/src/core/server/ui_settings/settings/theme.ts b/src/core/server/ui_settings/settings/theme.ts index 1c2f8417600df..35b8f0217c114 100644 --- a/src/core/server/ui_settings/settings/theme.ts +++ b/src/core/server/ui_settings/settings/theme.ts @@ -27,14 +27,14 @@ export const getThemeSettings = (): Record => { name: i18n.translate('core.ui_settings.params.themeVersionTitle', { defaultMessage: 'Theme version', }), - value: 'v7', + value: 'v8', type: 'select', - options: ['v7', 'v8 (beta)'], + options: ['v7', 'v8'], description: i18n.translate('core.ui_settings.params.themeVersionText', { defaultMessage: `Switch between the theme used for the current and next version of Kibana. A page refresh is required for the setting to be applied.`, }), requiresPageReload: true, - schema: schema.oneOf([schema.literal('v7'), schema.literal('v8 (beta)')]), + schema: schema.oneOf([schema.literal('v7'), schema.literal('v8')]), }, }; }; diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 14f614643ac9f..5e274712ad3a7 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -60,7 +60,6 @@ export function createRootWithSettings( configs: [], cliArgs: { dev: false, - quiet: false, silent: false, watch: false, basePath: false, diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index 973d71043f028..edff77d458f0f 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.ts +++ b/src/dev/build/tasks/build_kibana_platform_plugins.ts @@ -11,7 +11,12 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { lastValueFrom } from '@kbn/std'; import { CiStatsMetric } from '@kbn/dev-utils'; -import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; +import { + runOptimizer, + OptimizerConfig, + logOptimizerState, + reportOptimizerTimings, +} from '@kbn/optimizer'; import { Task, deleteAll, write, read } from '../lib'; @@ -30,7 +35,9 @@ export const BuildKibanaPlatformPlugins: Task = { limitsPath: Path.resolve(REPO_ROOT, 'packages/kbn-optimizer/limits.yml'), }); - await lastValueFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); + await lastValueFrom( + runOptimizer(config).pipe(logOptimizerState(log, config), reportOptimizerTimings(log, config)) + ); const combinedMetrics: CiStatsMetric[] = []; const metricFilePaths: string[] = []; diff --git a/src/dev/cli_dev_mode/get_server_watch_paths.test.ts b/src/dev/cli_dev_mode/get_server_watch_paths.test.ts index 7f84338b4efb8..ab113b96a5f03 100644 --- a/src/dev/cli_dev_mode/get_server_watch_paths.test.ts +++ b/src/dev/cli_dev_mode/get_server_watch_paths.test.ts @@ -28,7 +28,6 @@ it('produces the right watch and ignore list', () => { Array [ /src/core, /src/legacy/server, - /src/legacy/ui, /src/legacy/utils, /config, /x-pack/test/plugin_functional/plugins/resolver_test, diff --git a/src/dev/cli_dev_mode/get_server_watch_paths.ts b/src/dev/cli_dev_mode/get_server_watch_paths.ts index 4e00dd4ca98b9..46aa15659a513 100644 --- a/src/dev/cli_dev_mode/get_server_watch_paths.ts +++ b/src/dev/cli_dev_mode/get_server_watch_paths.ts @@ -41,7 +41,6 @@ export function getServerWatchPaths({ pluginPaths, pluginScanDirs }: Options) { [ fromRoot('src/core'), fromRoot('src/legacy/server'), - fromRoot('src/legacy/ui'), fromRoot('src/legacy/utils'), fromRoot('config'), ...pluginPaths, diff --git a/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts b/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts new file mode 100644 index 0000000000000..095ee9e8f6091 --- /dev/null +++ b/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CollectorSet } from '../../../plugins/usage_collection/server/collector'; +import { loggerMock } from '../../../core/server/logging/logger.mock'; +import type { Usage } from './types'; + +const { makeUsageCollector } = new CollectorSet({ + logger: loggerMock.create(), + maximumWaitTimeForAllCollectorsInS: 0, +}); + +export const myCollector = makeUsageCollector({ + type: 'importing_from_export_collector', + isReady: () => true, + fetch() { + return { + some_field: 'abc', + }; + }, + schema: { + some_field: { + type: 'keyword', + }, + }, +}); diff --git a/src/fixtures/telemetry_collectors/imported_interface_from_export/types.ts b/src/fixtures/telemetry_collectors/imported_interface_from_export/types.ts new file mode 100644 index 0000000000000..c8dd38f414406 --- /dev/null +++ b/src/fixtures/telemetry_collectors/imported_interface_from_export/types.ts @@ -0,0 +1,9 @@ +/* + * 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 type { Usage } from './usage_type'; diff --git a/src/fixtures/telemetry_collectors/imported_interface_from_export/usage_type.ts b/src/fixtures/telemetry_collectors/imported_interface_from_export/usage_type.ts new file mode 100644 index 0000000000000..765b8901a83e1 --- /dev/null +++ b/src/fixtures/telemetry_collectors/imported_interface_from_export/usage_type.ts @@ -0,0 +1,11 @@ +/* + * 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 interface Usage { + some_field: string; +} diff --git a/src/fixtures/telemetry_collectors/stats_collector.ts b/src/fixtures/telemetry_collectors/stats_collector.ts new file mode 100644 index 0000000000000..55d447751d4b6 --- /dev/null +++ b/src/fixtures/telemetry_collectors/stats_collector.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { CollectorSet } from '../../plugins/usage_collection/server/collector'; +import { loggerMock } from '../../core/server/logging/logger.mock'; + +const { makeStatsCollector } = new CollectorSet({ + logger: loggerMock.create(), + maximumWaitTimeForAllCollectorsInS: 0, +}); + +interface Usage { + some_field: string; +} + +/** + * Stats Collectors are allowed with schema and without them. + * We should collect them when the schema is defined. + */ + +export const myCollectorWithSchema = makeStatsCollector({ + type: 'my_stats_collector_with_schema', + isReady: () => true, + fetch() { + return { + some_field: 'abc', + }; + }, + schema: { + some_field: { + type: 'keyword', + }, + }, +}); + +export const myCollectorWithoutSchema = makeStatsCollector({ + type: 'my_stats_collector_without_schema', + isReady: () => true, + fetch() { + return { + some_field: 'abc', + }; + }, +}); diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 57ecaeab209ed..3fe0f5899668f 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -33,7 +33,6 @@ declare module 'hapi' { interface Server { config: () => KibanaConfig; - logWithMetadata: (tags: string[], message: string, meta: Record) => void; newPlatform: KbnServer['newPlatform']; } } diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index d2eebb7b0cd23..4bc76b6a7706f 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -94,20 +94,13 @@ export default class KbnServer { async listen() { await this.ready(); - const { server, config } = this; + const { server } = this; if (process.env.isDevCliChild) { // help parent process know when we are ready process.send(['SERVER_LISTENING']); } - server.log( - ['listening', 'info'], - `Server running at ${server.info.uri}${ - config.get('server.rewriteBasePath') ? config.get('server.basePath') : '' - }` - ); - return server; } @@ -133,13 +126,6 @@ export default class KbnServer { const loggingConfig = config.get('logging'); const opsConfig = config.get('ops'); - const subset = { - ops: opsConfig, - logging: loggingConfig, - }; - const plain = JSON.stringify(subset, null, 2); - this.server.log(['info', 'config'], 'New logging configuration:\n' + plain); - reconfigureLogging(this.server, loggingConfig, opsConfig.interval); } } diff --git a/src/legacy/server/logging/index.js b/src/legacy/server/logging/index.js index 0a3d7e3e0a5a9..1b2ae59f4aa00 100644 --- a/src/legacy/server/logging/index.js +++ b/src/legacy/server/logging/index.js @@ -6,13 +6,9 @@ * Side Public License, v 1. */ -import { setupLogging, setupLoggingRotate, attachMetaData } from '@kbn/legacy-logging'; +import { setupLogging, setupLoggingRotate } from '@kbn/legacy-logging'; export async function loggingMixin(kbnServer, server, config) { - server.decorate('server', 'logWithMetadata', (tags, message, metadata = {}) => { - server.log(tags, attachMetaData(message, metadata)); - }); - const loggingConfig = config.get('logging'); const opsInterval = config.get('ops.interval'); diff --git a/src/legacy/ui/public/documentation_links/documentation_links.ts b/src/legacy/ui/public/documentation_links/documentation_links.ts deleted file mode 100644 index 933812d29e9bb..0000000000000 --- a/src/legacy/ui/public/documentation_links/documentation_links.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* - WARNING: The links in this file are validated during the docs build. This is accomplished with some regex magic that - looks for these particular constants. As a result, we should not add new constants or change the existing ones. - If you absolutely must make a change, talk to Clinton Gormley first so he can update his Perl scripts. - */ -export const DOC_LINK_VERSION = 'stub'; -export const ELASTIC_WEBSITE_URL = 'stub'; - -export const documentationLinks = {}; diff --git a/src/plugins/dashboard/public/application/dashboard_app_functions.ts b/src/plugins/dashboard/public/application/dashboard_app_functions.ts index be1eea0e17a33..6d51422d4bd23 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_functions.ts +++ b/src/plugins/dashboard/public/application/dashboard_app_functions.ts @@ -213,8 +213,8 @@ export const getOutputSubscription = ({ }), distinctUntilChanged((a, b) => deepEqual( - a.map((ip) => ip.id), - b.map((ip) => ip.id) + a.map((ip) => ip && ip.id), + b.map((ip) => ip && ip.id) ) ), // using switchMap for previous task cancellation diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts index a8c9b9144707d..ad4d7ff8d78e2 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts @@ -31,6 +31,20 @@ describe('filterMatchesIndex', () => { expect(filterMatchesIndex(filter, indexPattern)).toBe(true); }); + it('should return true if custom filter for the same index is passed', () => { + const filter = { meta: { index: 'foo', key: 'bar', type: 'custom' } } as Filter; + const indexPattern = { id: 'foo', fields: [{ name: 'bara' }] } as IIndexPattern; + + expect(filterMatchesIndex(filter, indexPattern)).toBe(true); + }); + + it('should return false if custom filter for a different index is passed', () => { + const filter = { meta: { index: 'foo', key: 'bar', type: 'custom' } } as Filter; + const indexPattern = { id: 'food', fields: [{ name: 'bara' }] } as IIndexPattern; + + expect(filterMatchesIndex(filter, indexPattern)).toBe(false); + }); + it('should return false if the filter key does not match a field name', () => { const filter = { meta: { index: 'foo', key: 'baz' } } as Filter; const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts index 9fd8567b76e2b..478263d5ce601 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts @@ -18,5 +18,12 @@ export function filterMatchesIndex(filter: Filter, indexPattern?: IIndexPattern if (!filter.meta?.key || !indexPattern) { return true; } + + // Fixes https://github.com/elastic/kibana/issues/89878 + // Custom filters may refer multiple fields. Validate the index id only. + if (filter.meta?.type === 'custom') { + return filter.meta.index === indexPattern.id; + } + return indexPattern.fields.some((field: IFieldType) => field.name === filter.meta.key); } diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts index 8ac5ffec850f6..b38dce247261c 100644 --- a/src/plugins/data/common/search/expressions/index.ts +++ b/src/plugins/data/common/search/expressions/index.ts @@ -8,6 +8,11 @@ export * from './kibana'; export * from './kibana_context'; +export * from './kql'; +export * from './lucene'; +export * from './query_to_ast'; +export * from './timerange_to_ast'; export * from './kibana_context_type'; export * from './esaggs'; export * from './utils'; +export * from './timerange'; diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index 982db7505a3cf..5c2e2f418e69c 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -12,11 +12,13 @@ import { ExpressionFunctionDefinition, ExecutionContext } from 'src/plugins/expr import { Adapters } from 'src/plugins/inspector/common'; import { Query, uniqFilters } from '../../query'; import { ExecutionContextSearch, KibanaContext } from './kibana_context_type'; +import { KibanaQueryOutput } from './kibana_context_type'; +import { KibanaTimerangeOutput } from './timerange'; interface Arguments { - q?: string | null; + q?: KibanaQueryOutput | null; filters?: string | null; - timeRange?: string | null; + timeRange?: KibanaTimerangeOutput | null; savedSearchId?: string | null; } @@ -46,7 +48,7 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { }), args: { q: { - types: ['string', 'null'], + types: ['kibana_query', 'null'], aliases: ['query', '_'], default: null, help: i18n.translate('data.search.functions.kibana_context.q.help', { @@ -61,7 +63,7 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { }), }, timeRange: { - types: ['string', 'null'], + types: ['timerange', 'null'], default: null, help: i18n.translate('data.search.functions.kibana_context.timeRange.help', { defaultMessage: 'Specify Kibana time range filter', @@ -77,8 +79,8 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { }, async fn(input, args, { getSavedObject }) { - const timeRange = getParsedValue(args.timeRange, input?.timeRange); - let queries = mergeQueries(input?.query, getParsedValue(args?.q, [])); + const timeRange = args.timeRange || input?.timeRange; + let queries = mergeQueries(input?.query, args?.q || []); let filters = [...(input?.filters || []), ...getParsedValue(args?.filters, [])]; if (args.savedSearchId) { diff --git a/src/plugins/data/common/search/expressions/kibana_context_type.ts b/src/plugins/data/common/search/expressions/kibana_context_type.ts index 40adbc65317ad..090f09f7004ca 100644 --- a/src/plugins/data/common/search/expressions/kibana_context_type.ts +++ b/src/plugins/data/common/search/expressions/kibana_context_type.ts @@ -22,6 +22,8 @@ export type ExpressionValueSearchContext = ExpressionValueBoxed< ExecutionContextSearch >; +export type KibanaQueryOutput = ExpressionValueBoxed<'kibana_query', Query>; + // TODO: These two are exported for legacy reasons - remove them eventually. export type KIBANA_CONTEXT_NAME = 'kibana_context'; export type KibanaContext = ExpressionValueSearchContext; diff --git a/src/plugins/data/common/search/expressions/kql.test.ts b/src/plugins/data/common/search/expressions/kql.test.ts new file mode 100644 index 0000000000000..dcf3906e6c2f5 --- /dev/null +++ b/src/plugins/data/common/search/expressions/kql.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ExecutionContext } from 'src/plugins/expressions/common'; +import { ExpressionValueSearchContext } from './kibana_context_type'; +import { functionWrapper } from './utils'; +import { kqlFunction } from './kql'; + +describe('interpreter/functions#kql', () => { + const fn = functionWrapper(kqlFunction); + let input: Partial; + let context: ExecutionContext; + + beforeEach(() => { + input = { timeRange: { from: '0', to: '1' } }; + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn(input, { q: 'test' }, context); + expect(actual).toMatchInlineSnapshot( + ` + Object { + "language": "kuery", + "query": "test", + "type": "kibana_query", + } + ` + ); + }); +}); diff --git a/src/plugins/data/common/search/expressions/kql.ts b/src/plugins/data/common/search/expressions/kql.ts new file mode 100644 index 0000000000000..5dd830f92f834 --- /dev/null +++ b/src/plugins/data/common/search/expressions/kql.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { KibanaQueryOutput } from './kibana_context_type'; + +interface Arguments { + q: string; +} + +export type ExpressionFunctionKql = ExpressionFunctionDefinition< + 'kql', + null, + Arguments, + KibanaQueryOutput +>; + +export const kqlFunction: ExpressionFunctionKql = { + name: 'kql', + type: 'kibana_query', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.kql.help', { + defaultMessage: 'Create kibana kql query', + }), + args: { + q: { + types: ['string'], + required: true, + aliases: ['query', '_'], + help: i18n.translate('data.search.functions.kql.q.help', { + defaultMessage: 'Specify Kibana KQL free form text query', + }), + }, + }, + + fn(input, args) { + return { + type: 'kibana_query', + language: 'kuery', + query: args.q, + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/lucene.test.ts b/src/plugins/data/common/search/expressions/lucene.test.ts new file mode 100644 index 0000000000000..d0b26aad98ed8 --- /dev/null +++ b/src/plugins/data/common/search/expressions/lucene.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ExecutionContext } from 'src/plugins/expressions/common'; +import { ExpressionValueSearchContext } from './kibana_context_type'; +import { functionWrapper } from './utils'; +import { luceneFunction } from './lucene'; + +describe('interpreter/functions#lucene', () => { + const fn = functionWrapper(luceneFunction); + let input: Partial; + let context: ExecutionContext; + + beforeEach(() => { + input = { timeRange: { from: '0', to: '1' } }; + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn(input, { q: '{ "test": 1 }' }, context); + expect(actual).toMatchInlineSnapshot(` + Object { + "language": "lucene", + "query": Object { + "test": 1, + }, + "type": "kibana_query", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/lucene.ts b/src/plugins/data/common/search/expressions/lucene.ts new file mode 100644 index 0000000000000..a00ff7ed5f447 --- /dev/null +++ b/src/plugins/data/common/search/expressions/lucene.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { KibanaQueryOutput } from './kibana_context_type'; + +interface Arguments { + q: string; +} + +export type ExpressionFunctionLucene = ExpressionFunctionDefinition< + 'lucene', + null, + Arguments, + KibanaQueryOutput +>; + +export const luceneFunction: ExpressionFunctionLucene = { + name: 'lucene', + type: 'kibana_query', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.lucene.help', { + defaultMessage: 'Create kibana lucene query', + }), + args: { + q: { + types: ['string'], + required: true, + aliases: ['query', '_'], + help: i18n.translate('data.search.functions.lucene.q.help', { + defaultMessage: 'Specify Lucene free form text query', + }), + }, + }, + + fn(input, args) { + return { + type: 'kibana_query', + language: 'lucene', + query: JSON.parse(args.q), + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/query_to_ast.test.ts b/src/plugins/data/common/search/expressions/query_to_ast.test.ts new file mode 100644 index 0000000000000..4b9c97e99e7c7 --- /dev/null +++ b/src/plugins/data/common/search/expressions/query_to_ast.test.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 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 { queryToAst } from './query_to_ast'; + +describe('queryToAst', () => { + it('returns an object with the correct structure for lucene queies', () => { + const actual = queryToAst({ language: 'lucene', query: { country: 'US' } }); + expect(actual).toHaveProperty('functions'); + expect(actual.functions[0]).toHaveProperty('name', 'lucene'); + expect(actual.functions[0]).toHaveProperty('arguments', { + q: ['{"country":"US"}'], + }); + }); + + it('returns an object with the correct structure for kql queies', () => { + const actual = queryToAst({ language: 'kuery', query: 'country:US' }); + expect(actual).toHaveProperty('functions'); + expect(actual.functions[0]).toHaveProperty('name', 'kql'); + expect(actual.functions[0]).toHaveProperty('arguments', { + q: ['country:US'], + }); + }); +}); diff --git a/src/plugins/data/common/search/expressions/query_to_ast.ts b/src/plugins/data/common/search/expressions/query_to_ast.ts new file mode 100644 index 0000000000000..a9a6583f566c8 --- /dev/null +++ b/src/plugins/data/common/search/expressions/query_to_ast.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { buildExpression, buildExpressionFunction } from '../../../../expressions/common'; +import { Query } from '../../query'; +import { ExpressionFunctionKql } from './kql'; +import { ExpressionFunctionLucene } from './lucene'; + +export const queryToAst = (query: Query) => { + if (query.language === 'kuery') { + return buildExpression([ + buildExpressionFunction('kql', { q: query.query as string }), + ]); + } + return buildExpression([ + buildExpressionFunction('lucene', { q: JSON.stringify(query.query) }), + ]); +}; diff --git a/src/plugins/data/common/search/expressions/timerange.test.ts b/src/plugins/data/common/search/expressions/timerange.test.ts new file mode 100644 index 0000000000000..ae461b482e182 --- /dev/null +++ b/src/plugins/data/common/search/expressions/timerange.test.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 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 { ExecutionContext } from 'src/plugins/expressions/common'; +import { ExpressionValueSearchContext } from './kibana_context_type'; +import { functionWrapper } from './utils'; +import { kibanaTimerangeFunction } from './timerange'; + +describe('interpreter/functions#timerange', () => { + const fn = functionWrapper(kibanaTimerangeFunction); + let input: Partial; + let context: ExecutionContext; + + beforeEach(() => { + input = { timeRange: { from: '0', to: '1' } }; + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn(input, { from: 'now', to: 'now-7d', mode: 'absolute' }, context); + expect(actual).toMatchInlineSnapshot( + ` + Object { + "from": "now", + "mode": "absolute", + "to": "now-7d", + "type": "timerange", + } + ` + ); + }); +}); diff --git a/src/plugins/data/common/search/expressions/timerange.ts b/src/plugins/data/common/search/expressions/timerange.ts new file mode 100644 index 0000000000000..ed09bab629519 --- /dev/null +++ b/src/plugins/data/common/search/expressions/timerange.ts @@ -0,0 +1,61 @@ +/* + * 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'; +import { ExpressionFunctionDefinition, ExpressionValueBoxed } from 'src/plugins/expressions/common'; +import { TimeRange } from '../../query'; + +export type KibanaTimerangeOutput = ExpressionValueBoxed<'timerange', TimeRange>; + +export type ExpressionFunctionKibanaTimerange = ExpressionFunctionDefinition< + 'timerange', + null, + TimeRange, + KibanaTimerangeOutput +>; + +export const kibanaTimerangeFunction: ExpressionFunctionKibanaTimerange = { + name: 'timerange', + type: 'timerange', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.timerange.help', { + defaultMessage: 'Create kibana timerange', + }), + args: { + from: { + types: ['string'], + required: true, + help: i18n.translate('data.search.functions.timerange.from.help', { + defaultMessage: 'Specify the start date', + }), + }, + to: { + types: ['string'], + required: true, + help: i18n.translate('data.search.functions.timerange.to.help', { + defaultMessage: 'Specify the end date', + }), + }, + mode: { + types: ['string'], + options: ['absolute', 'relative'], + help: i18n.translate('data.search.functions.timerange.mode.help', { + defaultMessage: 'Specify the mode (absolute or relative)', + }), + }, + }, + + fn(input, args) { + return { + type: 'timerange', + from: args.from, + to: args.to, + mode: args.mode, + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/timerange_to_ast.test.ts b/src/plugins/data/common/search/expressions/timerange_to_ast.test.ts new file mode 100644 index 0000000000000..12ba1e012bb65 --- /dev/null +++ b/src/plugins/data/common/search/expressions/timerange_to_ast.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { timerangeToAst } from './timerange_to_ast'; + +describe('timerangeToAst', () => { + it('returns an object with the correct structure', () => { + const actual = timerangeToAst({ from: 'now', to: 'now-7d', mode: 'absolute' }); + expect(actual).toHaveProperty('name', 'timerange'); + expect(actual).toHaveProperty('arguments', { + from: ['now'], + mode: ['absolute'], + to: ['now-7d'], + }); + }); +}); diff --git a/src/plugins/data/common/search/expressions/timerange_to_ast.ts b/src/plugins/data/common/search/expressions/timerange_to_ast.ts new file mode 100644 index 0000000000000..ad66c12e68c83 --- /dev/null +++ b/src/plugins/data/common/search/expressions/timerange_to_ast.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { buildExpressionFunction } from '../../../../expressions/common'; +import { TimeRange } from '../../query'; +import { ExpressionFunctionKibanaTimerange } from './timerange'; + +export const timerangeToAst = (timerange: TimeRange) => { + return buildExpressionFunction('timerange', { + from: timerange.from, + to: timerange.to, + mode: timerange.mode, + }); +}; diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 474ff3b1b9780..1b74cec2fc847 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -13,3 +13,4 @@ export * from './search_source'; export * from './tabify'; export * from './types'; export * from './utils'; +export * from './session'; diff --git a/src/plugins/data/common/search/session/index.ts b/src/plugins/data/common/search/session/index.ts new file mode 100644 index 0000000000000..042786719dbba --- /dev/null +++ b/src/plugins/data/common/search/session/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 './status'; +export * from './types'; + +export const SEARCH_SESSIONS_TABLE_ID = 'searchSessionsMgmtUiTable'; diff --git a/src/plugins/data/common/search/session/status.ts b/src/plugins/data/common/search/session/status.ts new file mode 100644 index 0000000000000..790adbe7ba000 --- /dev/null +++ b/src/plugins/data/common/search/session/status.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 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 enum SearchSessionStatus { + IN_PROGRESS = 'in_progress', + ERROR = 'error', + COMPLETE = 'complete', + CANCELLED = 'cancelled', + EXPIRED = 'expired', +} diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts new file mode 100644 index 0000000000000..f63d2dfec142c --- /dev/null +++ b/src/plugins/data/common/search/session/types.ts @@ -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 { SearchSessionStatus } from './status'; + +export const SEARCH_SESSION_TYPE = 'search-session'; +export interface SearchSessionSavedObjectAttributes { + sessionId: string; + /** + * User-facing session name to be displayed in session management + */ + name?: string; + /** + * App that created the session. e.g 'discover' + */ + appId?: string; + /** + * Creation time of the session + */ + created: string; + /** + * Last touch time of the session + */ + touched: string; + /** + * Expiration time of the session. Expiration itself is managed by Elasticsearch. + */ + expires: string; + /** + * status + */ + status: SearchSessionStatus; + /** + * urlGeneratorId + */ + urlGeneratorId?: string; + /** + * The application state that was used to create the session. + * Should be used, for example, to re-load an expired search session. + */ + initialState?: Record; + /** + * Application state that should be used to restore the session. + * For example, relative dates are conveted to absolute ones. + */ + restoreState?: Record; + /** + * Mapping of search request hashes to their corresponsing info (async search id, etc.) + */ + idMapping: Record; + + /** + * This value is true if the session was actively stored by the user. If it is false, the session may be purged by the system. + */ + persisted: boolean; + /** + * The realm type/name & username uniquely identifies the user who created this search session + */ + realmType?: string; + realmName?: string; + username?: string; +} + +export interface SearchSessionRequestInfo { + /** + * ID of the async search request + */ + id: string; + /** + * Search strategy used to submit the search request + */ + strategy: string; + /** + * status + */ + status: string; + /** + * An optional error. Set if status is set to error. + */ + error?: string; +} + +export interface SearchSessionFindOptions { + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + filter?: string; +} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 1ab8e29ff2fd1..1838ca43e8c23 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -40,6 +40,7 @@ import { } from '../common'; import { FilterLabel } from './ui'; +import { FilterItem } from './ui/filter_bar'; import { generateFilters, @@ -54,6 +55,7 @@ import { // Filter helpers namespace: export const esFilters = { FilterLabel, + FilterItem, FILTERS, FilterStateStore, @@ -92,7 +94,7 @@ export const esFilters = { extractTimeRange, }; -export { +export type { RangeFilter, RangeFilterMeta, RangeFilterParams, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index cd3947cced956..a61b8f400d285 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -90,6 +90,7 @@ import { SavedObjectReference } from 'src/core/types'; import { SavedObjectsClientContract } from 'src/core/public'; import { SavedObjectsFindOptions } from 'kibana/public'; import { SavedObjectsFindResponse } from 'kibana/server'; +import { SavedObjectsUpdateResponse } from 'kibana/server'; import { SchemaTypeError } from '@kbn/config-schema'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; @@ -726,6 +727,7 @@ export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition_2 JSX.Element; + FilterItem: (props: import("./ui/filter_bar/filter_item").FilterItemProps) => JSX.Element; FILTERS: typeof FILTERS; FilterStateStore: typeof FilterStateStore; buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; @@ -2377,6 +2379,7 @@ export type SearchRequest = Record; // // @public export interface SearchSessionInfoProvider { + appendSessionStartTimeToName?: boolean; getName: () => Promise; // (undocumented) getUrlGeneratorData: () => Promise<{ @@ -2648,55 +2651,55 @@ export const UI_SETTINGS: { // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:139:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:56:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:55:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:55:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:55:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:55:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:55:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:55:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:55:23 - (ae-forgotten-export) The symbol "extractTimeRange" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:127:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:127:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:127:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:127:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:168:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:211:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "extractTimeRange" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:129:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:129:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:129:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:129:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:213:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/search/session/session_service.ts:42:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/search/session/session_service.ts:55:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 6522cae3e044f..8eb73ba62244f 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -25,6 +25,9 @@ import { ISearchGeneric, SearchSourceDependencies, SearchSourceService, + kibanaTimerangeFunction, + luceneFunction, + kqlFunction, } from '../../common/search'; import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; @@ -102,6 +105,9 @@ export class SearchService implements Plugin { ); expressions.registerFunction(kibana); expressions.registerFunction(kibanaContextFunction); + expressions.registerFunction(luceneFunction); + expressions.registerFunction(kqlFunction); + expressions.registerFunction(kibanaTimerangeFunction); expressions.registerType(kibanaContext); expressions.registerFunction(esdsl); diff --git a/src/plugins/data/public/search/session/lib/session_name_formatter.ts b/src/plugins/data/public/search/session/lib/session_name_formatter.ts new file mode 100644 index 0000000000000..6ec33a2184415 --- /dev/null +++ b/src/plugins/data/public/search/session/lib/session_name_formatter.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 moment from 'moment'; + +export function formatSessionName( + sessionName: string, + opts: { sessionStartTime?: Date; appendStartTime?: boolean } +): string { + if (opts.sessionStartTime && opts.appendStartTime) { + sessionName = appendDate(sessionName, opts.sessionStartTime); + } + + return sessionName; +} + +function appendDate(sessionName: string, sessionStartTime: Date): string { + return `${sessionName} - ${moment(sessionStartTime).format(`L @ LT`)}`; +} diff --git a/src/plugins/data/public/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts index c615be641078b..8ee44cb2ca4ef 100644 --- a/src/plugins/data/public/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -19,6 +19,7 @@ export function getSessionsClientMock(): jest.Mocked { update: jest.fn(), extend: jest.fn(), delete: jest.fn(), + rename: jest.fn(), }; } @@ -30,6 +31,8 @@ export function getSessionServiceMock(): jest.Mocked { getSessionId: jest.fn(), getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), state$: new BehaviorSubject(SearchSessionState.None).asObservable(), + searchSessionName$: new BehaviorSubject(undefined).asObservable(), + renameCurrentSession: jest.fn(), trackSearch: jest.fn((searchDescriptor) => () => {}), destroy: jest.fn(), cancel: jest.fn(), diff --git a/src/plugins/data/public/search/session/search_session_state.test.ts b/src/plugins/data/public/search/session/search_session_state.test.ts index 7559a6e0fdc67..d702d5c71a24b 100644 --- a/src/plugins/data/public/search/session/search_session_state.test.ts +++ b/src/plugins/data/public/search/session/search_session_state.test.ts @@ -7,6 +7,26 @@ */ import { createSessionStateContainer, SearchSessionState } from './search_session_state'; +import { SearchSessionSavedObject } from './sessions_client'; +import { SearchSessionStatus } from '../../../common'; + +const mockSavedObject: SearchSessionSavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: 'search-session', + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + sessionId: 'session_id', + touched: new Date().toISOString(), + created: new Date().toISOString(), + expires: new Date().toISOString(), + status: SearchSessionStatus.COMPLETE, + persisted: true, + }, + references: [], +}; describe('Session state container', () => { const appName = 'appName'; @@ -66,13 +86,13 @@ describe('Session state container', () => { }); test('store -> completed', () => { - expect(() => state.transitions.store()).toThrowError(); + expect(() => state.transitions.store(mockSavedObject)).toThrowError(); state.transitions.start({ appName }); const search = {}; state.transitions.trackSearch(search); expect(state.selectors.getState()).toBe(SearchSessionState.Loading); - state.transitions.store(); + state.transitions.store(mockSavedObject); expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundLoading); state.transitions.unTrackSearch(search); expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundCompleted); @@ -84,7 +104,7 @@ describe('Session state container', () => { const search = {}; state.transitions.trackSearch(search); expect(state.selectors.getState()).toBe(SearchSessionState.Loading); - state.transitions.store(); + state.transitions.store(mockSavedObject); expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundLoading); state.transitions.cancel(); expect(state.selectors.getState()).toBe(SearchSessionState.Canceled); @@ -106,7 +126,7 @@ describe('Session state container', () => { state.transitions.unTrackSearch(search); expect(state.selectors.getState()).toBe(SearchSessionState.Restored); - expect(() => state.transitions.store()).toThrowError(); + expect(() => state.transitions.store(mockSavedObject)).toThrowError(); expect(state.selectors.getState()).toBe(SearchSessionState.Restored); expect(() => state.transitions.cancel()).toThrowError(); expect(state.selectors.getState()).toBe(SearchSessionState.Restored); diff --git a/src/plugins/data/public/search/session/search_session_state.ts b/src/plugins/data/public/search/session/search_session_state.ts index cd2561d52f00e..e58e1062091bf 100644 --- a/src/plugins/data/public/search/session/search_session_state.ts +++ b/src/plugins/data/public/search/session/search_session_state.ts @@ -10,6 +10,7 @@ import uuid from 'uuid'; import { Observable } from 'rxjs'; import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; import { createStateContainer, StateContainer } from '../../../../kibana_utils/public'; +import { SearchSessionSavedObject } from './sessions_client'; /** * Possible state that current session can be in @@ -78,6 +79,11 @@ export interface SessionStateInternal { */ isStored: boolean; + /** + * Saved object of a current search session + */ + searchSessionSavedObject?: SearchSessionSavedObject; + /** * Is this session a restored session (have these requests already been made, and we're just * looking to re-use the previous search IDs)? @@ -125,10 +131,13 @@ export interface SessionPureTransitions< start: (state: S) => ({ appName }: { appName: string }) => S; restore: (state: S) => (sessionId: string) => S; clear: (state: S) => () => S; - store: (state: S) => () => S; + store: (state: S) => (searchSessionSavedObject: SearchSessionSavedObject) => S; trackSearch: (state: S) => (search: SearchDescriptor) => S; unTrackSearch: (state: S) => (search: SearchDescriptor) => S; cancel: (state: S) => () => S; + setSearchSessionSavedObject: ( + state: S + ) => (searchSessionSavedObject: SearchSessionSavedObject) => S; } export const sessionPureTransitions: SessionPureTransitions = { @@ -145,13 +154,14 @@ export const sessionPureTransitions: SessionPureTransitions = { isStored: true, }), clear: (state) => () => createSessionDefaultState(), - store: (state) => () => { + store: (state) => (searchSessionSavedObject: SearchSessionSavedObject) => { if (!state.sessionId) throw new Error("Can't store session. Missing sessionId"); if (state.isStored || state.isRestore) throw new Error('Can\'t store because current session is already stored"'); return { ...state, isStored: true, + searchSessionSavedObject, }; }, trackSearch: (state) => (search) => { @@ -176,6 +186,21 @@ export const sessionPureTransitions: SessionPureTransitions = { pendingSearches: [], isCanceled: true, isStored: false, + searchSessionSavedObject: undefined, + }; + }, + setSearchSessionSavedObject: (state) => (searchSessionSavedObject: SearchSessionSavedObject) => { + if (!state.sessionId) + throw new Error( + "Can't add search session saved object session into the state. Missing sessionId" + ); + if (state.sessionId !== searchSessionSavedObject.attributes.sessionId) + throw new Error( + "Can't add search session saved object session into the state. SessionIds don't match." + ); + return { + ...state, + searchSessionSavedObject, }; }, }; @@ -222,6 +247,8 @@ export const createSessionStateContainer = ( stateContainer: SessionStateContainer; sessionState$: Observable; sessionStartTime$: Observable; + searchSessionSavedObject$: Observable; + searchSessionName$: Observable; } => { const stateContainer = createStateContainer( createSessionDefaultState(), @@ -242,9 +269,21 @@ export const createSessionStateContainer = ( shareReplay(1) ); + const searchSessionSavedObject$ = stateContainer.state$.pipe( + map(() => stateContainer.get().searchSessionSavedObject), + distinctUntilChanged(), + shareReplay(1) + ); + + const searchSessionName$ = searchSessionSavedObject$.pipe( + map((savedObject) => savedObject?.attributes?.name) + ); + return { stateContainer, sessionState$, sessionStartTime$, + searchSessionSavedObject$, + searchSessionName$, }; }; diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index 32fdd9b6a52b1..13a1a1bd388ba 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -15,6 +15,27 @@ import { SearchSessionState } from './search_session_state'; import { createNowProviderMock } from '../../now_provider/mocks'; import { NowProviderInternalContract } from '../../now_provider'; import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants'; +import { SearchSessionSavedObject, ISessionsClient } from './sessions_client'; +import { SearchSessionStatus } from '../../../common'; +import { CoreStart } from 'kibana/public'; + +const mockSavedObject: SearchSessionSavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: 'search-session', + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + sessionId: 'session_id', + touched: new Date().toISOString(), + created: new Date().toISOString(), + expires: new Date().toISOString(), + status: SearchSessionStatus.COMPLETE, + persisted: true, + }, + references: [], +}; describe('Session service', () => { let sessionService: ISessionService; @@ -22,18 +43,28 @@ describe('Session service', () => { let nowProvider: jest.Mocked; let userHasAccessToSearchSessions = true; let currentAppId$: BehaviorSubject; + let toastService: jest.Mocked; + let sessionsClient: jest.Mocked; beforeEach(() => { const initializerContext = coreMock.createPluginInitializerContext(); const startService = coreMock.createSetup().getStartServices; + const startServicesMock = coreMock.createStart(); + toastService = startServicesMock.notifications.toasts; nowProvider = createNowProviderMock(); currentAppId$ = new BehaviorSubject('app'); + sessionsClient = getSessionsClientMock(); + sessionsClient.get.mockImplementation(async (id) => ({ + ...mockSavedObject, + id, + attributes: { ...mockSavedObject.attributes, sessionId: id }, + })); sessionService = new SessionService( initializerContext, () => startService().then(([coreStart, ...rest]) => [ { - ...coreStart, + ...startServicesMock, application: { ...coreStart.application, currentAppId$, @@ -49,7 +80,7 @@ describe('Session service', () => { }, ...rest, ]), - getSessionsClientMock(), + sessionsClient, nowProvider, { freezeState: false } // needed to use mocks inside state container ); @@ -269,4 +300,26 @@ describe('Session service', () => { ); }); }); + + test("rename() doesn't throw in case rename failed but shows a toast instead", async () => { + const renameError = new Error('Haha'); + sessionsClient.rename.mockRejectedValue(renameError); + sessionService.enableStorage({ + getName: async () => 'Name', + getUrlGeneratorData: async () => ({ + urlGeneratorId: 'id', + initialState: {}, + restoreState: {}, + }), + }); + sessionService.start(); + await sessionService.save(); + await expect(sessionService.renameCurrentSession('New name')).resolves.toBeUndefined(); + expect(toastService.addError).toHaveBeenCalledWith( + renameError, + expect.objectContaining({ + title: expect.stringContaining('Failed to edit name of the search session'), + }) + ); + }); }); diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 430fc8913c5fd..785b9357fc895 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -9,7 +9,12 @@ import { PublicContract } from '@kbn/utility-types'; import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { Observable, Subscription } from 'rxjs'; -import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; +import { + PluginInitializerContext, + StartServicesAccessor, + ToastsStart as ToastService, +} from 'kibana/public'; +import { i18n } from '@kbn/i18n'; import { UrlGeneratorId, UrlGeneratorStateMapping } from '../../../../share/public/'; import { ConfigSchema } from '../../../config'; import { @@ -21,6 +26,7 @@ import { ISessionsClient } from './sessions_client'; import { ISearchOptions } from '../../../common'; import { NowProviderInternalContract } from '../../now_provider'; import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants'; +import { formatSessionName } from './lib/session_name_formatter'; export type ISessionService = PublicContract; @@ -37,6 +43,13 @@ export interface SearchSessionInfoProvider Promise; + + /** + * Append session start time to a session name, + * `true` by default + */ + appendSessionStartTimeToName?: boolean; + getUrlGeneratorData: () => Promise<{ urlGeneratorId: ID; initialState: UrlGeneratorStateMapping[ID]['State']; @@ -65,12 +78,15 @@ export class SessionService { public readonly state$: Observable; private readonly state: SessionStateContainer; + public readonly searchSessionName$: Observable; private searchSessionInfoProvider?: SearchSessionInfoProvider; private searchSessionIndicatorUiConfig?: Partial; private subscription = new Subscription(); private currentApp?: string; private hasAccessToSearchSessions: boolean = false; + private toastService?: ToastService; + constructor( initializerContext: PluginInitializerContext, getStartServices: StartServicesAccessor, @@ -82,11 +98,13 @@ export class SessionService { stateContainer, sessionState$, sessionStartTime$, + searchSessionName$, } = createSessionStateContainer({ freeze: freezeState, }); this.state$ = sessionState$; this.state = stateContainer; + this.searchSessionName$ = searchSessionName$; this.subscription.add( sessionStartTime$.subscribe((startTime) => { @@ -100,6 +118,8 @@ export class SessionService { this.hasAccessToSearchSessions = coreStart.application.capabilities.management?.kibana?.[SEARCH_SESSIONS_MANAGEMENT_ID]; + this.toastService = coreStart.notifications.toasts; + this.subscription.add( coreStart.application.currentAppId$.subscribe((newAppName) => { this.currentApp = newAppName; @@ -198,6 +218,7 @@ export class SessionService { */ public restore(sessionId: string) { this.state.transitions.restore(sessionId); + this.refreshSearchSessionSavedObject(); } /** @@ -252,8 +273,13 @@ export class SessionService { currentSessionInfoProvider.getUrlGeneratorData(), ]); - await this.sessionsClient.create({ - name, + const formattedName = formatSessionName(name, { + sessionStartTime: this.state.get().startTime, + appendStartTime: currentSessionInfoProvider.appendSessionStartTimeToName, + }); + + const searchSessionSavedObject = await this.sessionsClient.create({ + name: formattedName, appId: currentSessionApp, restoreState: (restoreState as unknown) as Record, initialState: (initialState as unknown) as Record, @@ -263,7 +289,33 @@ export class SessionService { // if we are still interested in this result if (this.getSessionId() === sessionId) { - this.state.transitions.store(); + this.state.transitions.store(searchSessionSavedObject); + } + } + + /** + * Change user-facing name of a current session + * Doesn't throw in case of API error but presents a notification toast instead + * @param newName - new session name + */ + public async renameCurrentSession(newName: string) { + const sessionId = this.getSessionId(); + if (sessionId && this.state.get().isStored) { + let renamed = false; + try { + await this.sessionsClient.rename(sessionId, newName); + renamed = true; + } catch (e) { + this.toastService?.addError(e, { + title: i18n.translate('data.searchSessions.sessionService.sessionEditNameError', { + defaultMessage: 'Failed to edit name of the search session', + }), + }); + } + + if (renamed && sessionId === this.getSessionId()) { + await this.refreshSearchSessionSavedObject(); + } } } @@ -315,7 +367,10 @@ export class SessionService { searchSessionInfoProvider: SearchSessionInfoProvider, searchSessionIndicatorUiConfig?: SearchSessionIndicatorUiConfig ) { - this.searchSessionInfoProvider = searchSessionInfoProvider; + this.searchSessionInfoProvider = { + appendSessionStartTimeToName: true, + ...searchSessionInfoProvider, + }; this.searchSessionIndicatorUiConfig = searchSessionIndicatorUiConfig; } @@ -333,4 +388,23 @@ export class SessionService { ...this.searchSessionIndicatorUiConfig, }; } + + private async refreshSearchSessionSavedObject() { + const sessionId = this.getSessionId(); + if (sessionId && this.state.get().isStored) { + try { + const savedObject = await this.sessionsClient.get(sessionId); + if (this.getSessionId() === sessionId) { + // still interested in this result + this.state.transitions.setSearchSessionSavedObject(savedObject); + } + } catch (e) { + this.toastService?.addError(e, { + title: i18n.translate('data.searchSessions.sessionService.sessionObjectFetchError', { + defaultMessage: 'Failed to fetch search session info', + }), + }); + } + } + } } diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts index 1742db9d033bd..0b6f1b79f0c63 100644 --- a/src/plugins/data/public/search/session/sessions_client.ts +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -8,8 +8,13 @@ import { PublicContract } from '@kbn/utility-types'; import { HttpSetup, SavedObjectsFindOptions } from 'kibana/public'; -import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; - +import type { + SavedObject, + SavedObjectsFindResponse, + SavedObjectsUpdateResponse, +} from 'kibana/server'; +import type { SearchSessionSavedObjectAttributes } from '../../../common'; +export type SearchSessionSavedObject = SavedObject; export type ISessionsClient = PublicContract; export interface SessionsClientDeps { http: HttpSetup; @@ -25,7 +30,7 @@ export class SessionsClient { this.http = deps.http; } - public get(sessionId: string): Promise { + public get(sessionId: string): Promise { return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); } @@ -43,7 +48,7 @@ export class SessionsClient { restoreState: Record; urlGeneratorId: string; sessionId: string; - }): Promise { + }): Promise { return this.http.post(`/internal/session`, { body: JSON.stringify({ name, @@ -62,13 +67,26 @@ export class SessionsClient { }); } - public update(sessionId: string, attributes: unknown): Promise { + public update( + sessionId: string, + attributes: unknown + ): Promise> { return this.http!.put(`/internal/session/${encodeURIComponent(sessionId)}`, { body: JSON.stringify(attributes), }); } - public extend(sessionId: string, expires: string): Promise { + public rename( + sessionId: string, + newName: string + ): Promise>> { + return this.update(sessionId, { name: newName }); + } + + public extend( + sessionId: string, + expires: string + ): Promise> { return this.http!.post(`/internal/session/${encodeURIComponent(sessionId)}/_extend`, { body: JSON.stringify({ expires }), }); diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 05ae4e74535f4..5ad88e6fdf5be 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -25,7 +25,9 @@ import { } from '../../../common'; import { getIndexPatterns } from '../../services'; -interface Props { +type PanelOptions = 'pinFilter' | 'editFilter' | 'negateFilter' | 'disableFilter' | 'deleteFilter'; + +export interface FilterItemProps { id: string; filter: Filter; indexPatterns: IIndexPattern[]; @@ -34,6 +36,7 @@ interface Props { onRemove: () => void; intl: InjectedIntl; uiSettings: IUiSettingsClient; + hiddenPanelOptions?: PanelOptions[]; } interface LabelOptions { @@ -53,10 +56,10 @@ export type FilterLabelStatus = export const FILTER_EDITOR_WIDTH = 800; -export function FilterItem(props: Props) { +export function FilterItem(props: FilterItemProps) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [indexPatternExists, setIndexPatternExists] = useState(undefined); - const { id, filter, indexPatterns } = props; + const { id, filter, indexPatterns, hiddenPanelOptions } = props; useEffect(() => { const index = props.filter.meta.index; @@ -154,83 +157,90 @@ export function FilterItem(props: Props) { function getPanels() { const { negate, disabled } = filter.meta; - return [ + let mainPanelItems = [ { - id: 0, - items: [ - { - name: isFilterPinned(filter) - ? props.intl.formatMessage({ - id: 'data.filter.filterBar.unpinFilterButtonLabel', - defaultMessage: 'Unpin', - }) - : props.intl.formatMessage({ - id: 'data.filter.filterBar.pinFilterButtonLabel', - defaultMessage: 'Pin across all apps', - }), - icon: 'pin', - onClick: () => { - setIsPopoverOpen(false); - onTogglePinned(); - }, - 'data-test-subj': 'pinFilter', - }, - { - name: props.intl.formatMessage({ - id: 'data.filter.filterBar.editFilterButtonLabel', - defaultMessage: 'Edit filter', + name: isFilterPinned(filter) + ? props.intl.formatMessage({ + id: 'data.filter.filterBar.unpinFilterButtonLabel', + defaultMessage: 'Unpin', + }) + : props.intl.formatMessage({ + id: 'data.filter.filterBar.pinFilterButtonLabel', + defaultMessage: 'Pin across all apps', }), - icon: 'pencil', - panel: 1, - 'data-test-subj': 'editFilter', - }, - { - name: negate - ? props.intl.formatMessage({ - id: 'data.filter.filterBar.includeFilterButtonLabel', - defaultMessage: 'Include results', - }) - : props.intl.formatMessage({ - id: 'data.filter.filterBar.excludeFilterButtonLabel', - defaultMessage: 'Exclude results', - }), - icon: negate ? 'plusInCircle' : 'minusInCircle', - onClick: () => { - setIsPopoverOpen(false); - onToggleNegated(); - }, - 'data-test-subj': 'negateFilter', - }, - { - name: disabled - ? props.intl.formatMessage({ - id: 'data.filter.filterBar.enableFilterButtonLabel', - defaultMessage: 'Re-enable', - }) - : props.intl.formatMessage({ - id: 'data.filter.filterBar.disableFilterButtonLabel', - defaultMessage: 'Temporarily disable', - }), - icon: `${disabled ? 'eye' : 'eyeClosed'}`, - onClick: () => { - setIsPopoverOpen(false); - onToggleDisabled(); - }, - 'data-test-subj': 'disableFilter', - }, - { - name: props.intl.formatMessage({ - id: 'data.filter.filterBar.deleteFilterButtonLabel', - defaultMessage: 'Delete', + icon: 'pin', + onClick: () => { + setIsPopoverOpen(false); + onTogglePinned(); + }, + 'data-test-subj': 'pinFilter', + }, + { + name: props.intl.formatMessage({ + id: 'data.filter.filterBar.editFilterButtonLabel', + defaultMessage: 'Edit filter', + }), + icon: 'pencil', + panel: 1, + 'data-test-subj': 'editFilter', + }, + { + name: negate + ? props.intl.formatMessage({ + id: 'data.filter.filterBar.includeFilterButtonLabel', + defaultMessage: 'Include results', + }) + : props.intl.formatMessage({ + id: 'data.filter.filterBar.excludeFilterButtonLabel', + defaultMessage: 'Exclude results', + }), + icon: negate ? 'plusInCircle' : 'minusInCircle', + onClick: () => { + setIsPopoverOpen(false); + onToggleNegated(); + }, + 'data-test-subj': 'negateFilter', + }, + { + name: disabled + ? props.intl.formatMessage({ + id: 'data.filter.filterBar.enableFilterButtonLabel', + defaultMessage: 'Re-enable', + }) + : props.intl.formatMessage({ + id: 'data.filter.filterBar.disableFilterButtonLabel', + defaultMessage: 'Temporarily disable', }), - icon: 'trash', - onClick: () => { - setIsPopoverOpen(false); - props.onRemove(); - }, - 'data-test-subj': 'deleteFilter', - }, - ], + icon: `${disabled ? 'eye' : 'eyeClosed'}`, + onClick: () => { + setIsPopoverOpen(false); + onToggleDisabled(); + }, + 'data-test-subj': 'disableFilter', + }, + { + name: props.intl.formatMessage({ + id: 'data.filter.filterBar.deleteFilterButtonLabel', + defaultMessage: 'Delete', + }), + icon: 'trash', + onClick: () => { + setIsPopoverOpen(false); + props.onRemove(); + }, + 'data-test-subj': 'deleteFilter', + }, + ]; + + if (hiddenPanelOptions && hiddenPanelOptions.length > 0) { + mainPanelItems = mainPanelItems.filter( + (pItem) => !hiddenPanelOptions.includes(pItem['data-test-subj'] as PanelOptions) + ); + } + return [ + { + id: 0, + items: mainPanelItems, }, { id: 1, @@ -363,3 +373,6 @@ export function FilterItem(props: Props) { ); } + +// eslint-disable-next-line import/no-default-export +export default FilterItem; diff --git a/src/plugins/data/public/ui/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx index a4a6eb3b50a31..4065c3b8fe0df 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_options.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_options.tsx @@ -27,6 +27,8 @@ interface State { } class FilterOptionsUI extends Component { + private buttonRef = React.createRef(); + public state: State = { isPopoverOpen: false, }; @@ -39,6 +41,7 @@ class FilterOptionsUI extends Component { public closePopover = () => { this.setState({ isPopoverOpen: false }); + this.buttonRef.current?.focus(); }; public render() { @@ -151,6 +154,7 @@ class FilterOptionsUI extends Component { defaultMessage: 'Change all filters', })} data-test-subj="showFilterActions" + buttonRef={this.buttonRef} /> } anchorPosition="rightUp" diff --git a/src/plugins/data/public/ui/filter_bar/index.tsx b/src/plugins/data/public/ui/filter_bar/index.tsx index f8c2f46424c38..9fb352c64aa5d 100644 --- a/src/plugins/data/public/ui/filter_bar/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/index.tsx @@ -17,3 +17,12 @@ export const FilterLabel = (props: FilterLabelProps) => ( ); + +import type { FilterItemProps } from './filter_item'; + +const LazyFilterItem = React.lazy(() => import('./filter_item')); +export const FilterItem = (props: FilterItemProps) => ( + }> + + +); diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index 6f088fe641c51..c758ace75bd60 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -26,18 +26,18 @@ padding-top: $euiSizeS + 3px; box-shadow: 0 0 0 1px $euiFormBorderColor; - &:not(:focus):not(:invalid) { + &:not(.kbnQueryBar__textarea--autoHeight):not(:invalid) { @include euiYScrollWithShadows; } - &:not(:focus) { + &:not(.kbnQueryBar__textarea--autoHeight) { white-space: nowrap; overflow-y: hidden; overflow-x: hidden; } // When focused, let it scroll - &:focus { + &.kbnQueryBar__textarea--autoHeight { overflow-x: auto; overflow-y: auto; white-space: normal; diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx index 053ca6f78e910..65e84612bc508 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx @@ -86,6 +86,8 @@ export function QueryLanguageSwitcher({ isOpen={isPopoverOpen} closePopover={() => setIsPopoverOpen(false)} repositionOnScroll + ownFocus={true} + initialFocus={'[role="switch"]'} > ({ clear: jest.fn(), }); -function wrapQueryStringInputInContext(testProps: any, storage?: any) { - const defaultOptions = { - screenTitle: 'Another Screen', - intl: null as any, - }; +const QueryStringInput = withKibana(QueryStringInputUI); +function wrapQueryStringInputInContext(testProps: any, storage?: any) { const services = { ...startMock, data: dataPluginMock.createStartContract(), @@ -75,6 +73,11 @@ function wrapQueryStringInputInContext(testProps: any, storage?: any) { storage: storage || createMockStorage(), }; + const defaultOptions = { + screenTitle: 'Another Screen', + intl: null as any, + }; + return ( @@ -84,15 +87,12 @@ function wrapQueryStringInputInContext(testProps: any, storage?: any) { ); } -// FAILING: https://github.com/elastic/kibana/issues/85715 -// FAILING: https://github.com/elastic/kibana/issues/89603 -// FAILING: https://github.com/elastic/kibana/issues/89641 -describe.skip('QueryStringInput', () => { +describe('QueryStringInput', () => { beforeEach(() => { jest.clearAllMocks(); }); - it.skip('Should render the given query', async () => { + it('Should render the given query', async () => { const { getByText } = render( wrapQueryStringInputInContext({ query: kqlQuery, @@ -228,7 +228,7 @@ describe.skip('QueryStringInput', () => { expect(mockCallback).toHaveBeenCalledWith(); }); - it('Should fire onChangeQueryInputFocus callback on input blur', () => { + it('Should fire onChangeQueryInputFocus after a delay', () => { const mockCallback = jest.fn(); const component = mount( @@ -243,10 +243,93 @@ describe.skip('QueryStringInput', () => { const inputWrapper = component.find(EuiTextArea).find('textarea'); inputWrapper.simulate('blur'); + jest.advanceTimersByTime(10); + + expect(mockCallback).toHaveBeenCalledTimes(0); + + jest.advanceTimersByTime(100); + expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockCallback).toHaveBeenCalledWith(false); }); + it('Should not fire onChangeQueryInputFocus if input is focused back', () => { + const mockCallback = jest.fn(); + + const component = mount( + wrapQueryStringInputInContext({ + query: kqlQuery, + onChangeQueryInputFocus: mockCallback, + indexPatterns: [stubIndexPatternWithFields], + disableAutoFocus: true, + }) + ); + + const inputWrapper = component.find(EuiTextArea).find('textarea'); + inputWrapper.simulate('blur'); + + jest.advanceTimersByTime(5); + expect(mockCallback).toHaveBeenCalledTimes(0); + + inputWrapper.simulate('focus'); + + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith(true); + + jest.advanceTimersByTime(100); + expect(mockCallback).toHaveBeenCalledTimes(1); + }); + + it('Should call onSubmit after a delay when submitOnBlur is on and blurs input', () => { + const mockCallback = jest.fn(); + + const component = mount( + wrapQueryStringInputInContext({ + query: kqlQuery, + onSubmit: mockCallback, + indexPatterns: [stubIndexPatternWithFields], + disableAutoFocus: true, + submitOnBlur: true, + }) + ); + + const inputWrapper = component.find(EuiTextArea).find('textarea'); + inputWrapper.simulate('blur'); + + jest.advanceTimersByTime(10); + + expect(mockCallback).toHaveBeenCalledTimes(0); + + jest.advanceTimersByTime(100); + + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith(kqlQuery); + }); + + it("Shouldn't call onSubmit on blur by default", () => { + const mockCallback = jest.fn(); + + const component = mount( + wrapQueryStringInputInContext({ + query: kqlQuery, + onSubmit: mockCallback, + indexPatterns: [stubIndexPatternWithFields], + disableAutoFocus: true, + }) + ); + + const inputWrapper = component.find(EuiTextArea).find('textarea'); + inputWrapper.simulate('blur'); + + jest.advanceTimersByTime(10); + + expect(mockCallback).toHaveBeenCalledTimes(0); + + jest.advanceTimersByTime(100); + + expect(mockCallback).toHaveBeenCalledTimes(0); + }); + it('Should use PersistedLog for recent search suggestions', async () => { const component = mount( wrapQueryStringInputInContext({ diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 65f7e4f3964cd..5e34c401c7615 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -123,6 +123,12 @@ export default class QueryStringInputUI extends Component { private componentIsUnmounting = false; private queryBarInputDivRefInstance: RefObject = createRef(); + /** + * If any element within the container is currently focused + * @private + */ + private isFocusWithin = false; + private getQueryString = () => { return toUser(this.props.query.query); }; @@ -492,30 +498,37 @@ export default class QueryStringInputUI extends Component { private onOutsideClick = () => { if (this.state.isSuggestionsVisible) { this.setState({ isSuggestionsVisible: false, index: null }); - } - this.handleBlurHeight(); - if (this.props.onChangeQueryInputFocus) { - this.props.onChangeQueryInputFocus(false); + this.scheduleOnInputBlur(); } }; + private blurTimeoutHandle: number | undefined; + /** + * Notify parent about input's blur after a delay only + * if the focus didn't get back inside the input container + * and if suggestions were closed + * https://github.com/elastic/kibana/issues/92040 + */ + private scheduleOnInputBlur = () => { + clearTimeout(this.blurTimeoutHandle); + this.blurTimeoutHandle = window.setTimeout(() => { + if (!this.isFocusWithin && !this.state.isSuggestionsVisible && !this.componentIsUnmounting) { + this.handleBlurHeight(); + if (this.props.onChangeQueryInputFocus) { + this.props.onChangeQueryInputFocus(false); + } + + if (this.props.submitOnBlur) { + this.onSubmit(this.props.query); + } + } + }, 50); + }; + private onInputBlur = () => { - this.handleBlurHeight(); - if (this.props.onChangeQueryInputFocus) { - this.props.onChangeQueryInputFocus(false); - } if (isFunction(this.props.onBlur)) { this.props.onBlur(); } - if (this.props.submitOnBlur) { - // Input blur triggers when the user selects something from autocomplete, so wait a bit to ensure that - // the entire QueryStringInput component has actually blurred (e.g. from user clicking or tabbing away) - setTimeout(() => { - if (document.activeElement !== this.inputRef) { - this.onSubmit(this.props.query); - } - }, 200); - } }; private onClickSuggestion = (suggestion: QuerySuggestion, index: number) => { @@ -604,6 +617,7 @@ export default class QueryStringInputUI extends Component { handleAutoHeight = () => { if (this.inputRef !== null && document.activeElement === this.inputRef) { + this.inputRef.classList.add('kbnQueryBar__textarea--autoHeight'); this.inputRef.style.setProperty('height', `${this.inputRef.scrollHeight}px`, 'important'); } this.handleListUpdate(); @@ -612,6 +626,7 @@ export default class QueryStringInputUI extends Component { handleRemoveHeight = () => { if (this.inputRef !== null) { this.inputRef.style.removeProperty('height'); + this.inputRef.classList.remove('kbnQueryBar__textarea--autoHeight'); } }; @@ -648,7 +663,16 @@ export default class QueryStringInputUI extends Component { ); return ( -
+
{ + this.isFocusWithin = true; + }} + onBlur={(e) => { + this.isFocusWithin = false; + this.scheduleOnInputBlur(); + }} + > {this.props.prepend}
): void { - const router = http.createRouter(); + const router = http.createRouter(); registerValueSuggestionsRoute(router, config$); } diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 489a23eb83897..8e6d3afa18ed5 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -12,12 +12,12 @@ import { IRouter, SharedGlobalConfig } from 'kibana/server'; import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { IFieldType, Filter } from '../index'; -import { findIndexPatternById, getFieldByName } from '../index_patterns'; +import { IFieldType, Filter, ES_SEARCH_STRATEGY, IEsSearchRequest } from '../index'; import { getRequestAbortedSignal } from '../lib'; +import { DataRequestHandlerContext } from '../types'; export function registerValueSuggestionsRoute( - router: IRouter, + router: IRouter, config$: Observable ) { router.post( @@ -44,24 +44,40 @@ export function registerValueSuggestionsRoute( const config = await config$.pipe(first()).toPromise(); const { field: fieldName, query, filters } = request.body; const { index } = request.params; - const { client } = context.core.elasticsearch.legacy; const signal = getRequestAbortedSignal(request.events.aborted$); + if (!context.indexPatterns) { + return response.badRequest(); + } + const autocompleteSearchOptions = { timeout: `${config.kibana.autocompleteTimeout.asMilliseconds()}ms`, terminate_after: config.kibana.autocompleteTerminateAfter.asMilliseconds(), }; - const indexPattern = await findIndexPatternById(context.core.savedObjects.client, index); - - const field = indexPattern && getFieldByName(fieldName, indexPattern); + const indexPatterns = await context.indexPatterns.find(index, 1); + if (!indexPatterns || indexPatterns.length === 0) { + return response.notFound(); + } + const field = indexPatterns[0].getFieldByName(fieldName); const body = await getBody(autocompleteSearchOptions, field || fieldName, query, filters); - const result = await client.callAsCurrentUser('search', { index, body }, { signal }); + const searchRequest: IEsSearchRequest = { + params: { + index, + body, + }, + }; + const { rawResponse } = await context.search + .search(searchRequest, { + strategy: ES_SEARCH_STRATEGY, + abortSignal: signal, + }) + .toPromise(); const buckets: any[] = - get(result, 'aggregations.suggestions.buckets') || - get(result, 'aggregations.nestedSuggestions.suggestions.buckets'); + get(rawResponse, 'aggregations.suggestions.buckets') || + get(rawResponse, 'aggregations.nestedSuggestions.suggestions.buckets'); return response.ok({ body: map(buckets || [], 'key') }); } diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index cbf09ef57d96a..c153c0efa8892 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -236,10 +236,10 @@ export { SearchUsage, SearchSessionService, ISearchSessionService, - SearchRequestHandlerContext, - DataRequestHandlerContext, } from './search'; +export { DataRequestHandlerContext } from './types'; + // Search namespace export const search = { aggs: { diff --git a/src/plugins/data/server/index_patterns/index.ts b/src/plugins/data/server/index_patterns/index.ts index 7226d6f015cf8..85610cd85a3ce 100644 --- a/src/plugins/data/server/index_patterns/index.ts +++ b/src/plugins/data/server/index_patterns/index.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { IndexPatternsService } from '../../common/index_patterns'; + export * from './utils'; export { IndexPatternsFetcher, @@ -15,3 +17,5 @@ export { getCapabilitiesForRollupIndices, } from './fetcher'; export { IndexPatternsServiceProvider, IndexPatternsServiceStart } from './index_patterns_service'; + +export type IndexPatternsHandlerContext = IndexPatternsService; diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 5d703021b94da..b489c29bc3b70 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -25,6 +25,7 @@ import { getIndexPatternLoad } from './expressions'; import { UiSettingsServerToCommon } from './ui_settings_wrapper'; import { IndexPatternsApiServer } from './index_patterns_api_client'; import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; +import { DataRequestHandlerContext } from '../types'; export interface IndexPatternsServiceStart { indexPatternsServiceFactory: ( @@ -35,6 +36,7 @@ export interface IndexPatternsServiceStart { export interface IndexPatternsServiceSetupDeps { expressions: ExpressionsServerSetup; + logger: Logger; } export interface IndexPatternsServiceStartDeps { @@ -45,11 +47,27 @@ export interface IndexPatternsServiceStartDeps { export class IndexPatternsServiceProvider implements Plugin { public setup( core: CoreSetup, - { expressions }: IndexPatternsServiceSetupDeps + { logger, expressions }: IndexPatternsServiceSetupDeps ) { core.savedObjects.registerType(indexPatternSavedObjectType); core.capabilities.registerProvider(capabilitiesProvider); + core.http.registerRouteHandlerContext( + 'indexPatterns', + async (context, request) => { + const [coreStart, , dataStart] = await core.getStartServices(); + try { + return await dataStart.indexPatterns.indexPatternsServiceFactory( + coreStart.savedObjects.getScopedClient(request), + coreStart.elasticsearch.client.asScoped(request).asCurrentUser + ); + } catch (e) { + logger.error(e); + return undefined; + } + } + ); + registerRoutes(core.http, core.getStartServices); expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); diff --git a/src/plugins/data/server/mocks.ts b/src/plugins/data/server/mocks.ts index 786dd30dbabd0..c82db7a141403 100644 --- a/src/plugins/data/server/mocks.ts +++ b/src/plugins/data/server/mocks.ts @@ -13,7 +13,7 @@ import { } from './search/mocks'; import { createFieldFormatsSetupMock, createFieldFormatsStartMock } from './field_formats/mocks'; import { createIndexPatternsStartMock } from './index_patterns/mocks'; -import { DataRequestHandlerContext } from './search'; +import { DataRequestHandlerContext } from './types'; function createSetupContract() { return { diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index a7a7663d6981c..3408c39cbb8e2 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -82,7 +82,10 @@ export class DataServerPlugin this.queryService.setup(core); this.autocompleteService.setup(core); this.kqlTelemetryService.setup(core, { usageCollection }); - this.indexPatterns.setup(core, { expressions }); + this.indexPatterns.setup(core, { + expressions, + logger: this.logger.get('indexPatterns'), + }); core.uiSettings.register(getUiSettings()); diff --git a/src/plugins/data/server/search/routes/msearch.ts b/src/plugins/data/server/search/routes/msearch.ts index b578805d8c2df..b5f06c4b343e7 100644 --- a/src/plugins/data/server/search/routes/msearch.ts +++ b/src/plugins/data/server/search/routes/msearch.ts @@ -12,7 +12,7 @@ import { SearchRouteDependencies } from '../search_service'; import { getCallMsearch } from './call_msearch'; import { reportServerError } from '../../../../kibana_utils/server'; -import type { DataPluginRouter } from '../types'; +import type { DataPluginRouter } from '../../types'; /** * The msearch route takes in an array of searches, each consisting of header * and body json, and reformts them into a single request for the _msearch API. diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index 1680a9c4a7237..6690e2b81f3e4 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -10,7 +10,7 @@ import { first } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; import { getRequestAbortedSignal } from '../../lib'; import { reportServerError } from '../../../../kibana_utils/server'; -import type { DataPluginRouter } from '../types'; +import type { DataPluginRouter } from '../../types'; export function registerSearchRoute(router: DataPluginRouter): void { router.post( diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 6ece8ff945468..ab9fc84d51187 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -29,7 +29,6 @@ import type { ISearchStrategy, SearchEnhancements, SearchStrategyDependencies, - DataRequestHandlerContext, } from './types'; import { AggsService } from './aggs'; @@ -52,6 +51,9 @@ import { kibana, kibanaContext, kibanaContextFunction, + kibanaTimerangeFunction, + kqlFunction, + luceneFunction, SearchSourceDependencies, searchSourceRequiredUiSettings, SearchSourceService, @@ -66,6 +68,7 @@ import { ConfigSchema } from '../../config'; import { ISearchSessionService, SearchSessionService } from './session'; import { KbnServerError } from '../../../kibana_utils/server'; import { registerBsearchRoute } from './routes/bsearch'; +import { DataRequestHandlerContext } from '../types'; type StrategyMap = Record>; @@ -142,6 +145,9 @@ export class SearchService implements Plugin { expressions.registerFunction(getEsaggs({ getStartServices: core.getStartServices })); expressions.registerFunction(kibana); + expressions.registerFunction(luceneFunction); + expressions.registerFunction(kqlFunction); + expressions.registerFunction(kibanaTimerangeFunction); expressions.registerFunction(kibanaContextFunction); expressions.registerType(kibanaContext); diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index e8548257c0167..d7aadcc348c87 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -8,12 +8,10 @@ import { Observable } from 'rxjs'; import type { - IRouter, IScopedClusterClient, IUiSettingsClient, SavedObjectsClientContract, KibanaRequest, - RequestHandlerContext, } from 'src/core/server'; import { ISearchOptions, @@ -116,12 +114,3 @@ export interface ISearchStart< } export type SearchRequestHandlerContext = IScopedSearchClient; - -/** - * @internal - */ -export interface DataRequestHandlerContext extends RequestHandlerContext { - search: SearchRequestHandlerContext; -} - -export type DataPluginRouter = IRouter; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 0118906c181cc..83f7c67eba057 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -312,6 +312,12 @@ export const config: PluginConfigDescriptor; // @internal (undocumented) export interface DataRequestHandlerContext extends RequestHandlerContext { + // Warning: (ae-forgotten-export) The symbol "IndexPatternsHandlerContext" needs to be exported by the entry point index.d.ts + // + // (undocumented) + indexPatterns?: IndexPatternsHandlerContext; + // Warning: (ae-forgotten-export) The symbol "SearchRequestHandlerContext" needs to be exported by the entry point index.d.ts + // // (undocumented) search: SearchRequestHandlerContext; } @@ -954,7 +960,7 @@ export class IndexPatternsServiceProvider implements Plugin_3, { expressions }: IndexPatternsServiceSetupDeps): void; + setup(core: CoreSetup_2, { logger, expressions }: IndexPatternsServiceSetupDeps): void; // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStartDeps" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1319,11 +1325,6 @@ export const search: { tabifyGetColumns: typeof tabifyGetColumns; }; -// Warning: (ae-missing-release-tag) "SearchRequestHandlerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type SearchRequestHandlerContext = IScopedSearchClient; - // @internal export class SearchSessionService implements ISearchSessionService { constructor(); @@ -1515,7 +1516,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:79:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:114:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:112:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/server/types.ts b/src/plugins/data/server/types.ts new file mode 100644 index 0000000000000..ea0fa49058d37 --- /dev/null +++ b/src/plugins/data/server/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { IRouter, RequestHandlerContext } from 'src/core/server'; + +import { SearchRequestHandlerContext } from './search'; +import { IndexPatternsHandlerContext } from './index_patterns'; + +/** + * @internal + */ +export interface DataRequestHandlerContext extends RequestHandlerContext { + search: SearchRequestHandlerContext; + indexPatterns?: IndexPatternsHandlerContext; +} + +export type DataPluginRouter = IRouter; diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index b1d5b87082696..23589c22b3371 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -417,7 +417,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.format.numberFormat.numeralFormatLinkText', values: { numeralFormatLink: - '' + + '' + i18n.translate('data.advancedSettings.format.numberFormat.numeralFormatLinkText', { defaultMessage: 'numeral format', }) + @@ -439,7 +439,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.format.percentFormat.numeralFormatLinkText', values: { numeralFormatLink: - '' + + '' + i18n.translate('data.advancedSettings.format.percentFormat.numeralFormatLinkText', { defaultMessage: 'numeral format', }) + @@ -461,7 +461,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.format.bytesFormat.numeralFormatLinkText', values: { numeralFormatLink: - '' + + '' + i18n.translate('data.advancedSettings.format.bytesFormat.numeralFormatLinkText', { defaultMessage: 'numeral format', }) + @@ -483,7 +483,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.format.currencyFormat.numeralFormatLinkText', values: { numeralFormatLink: - '' + + '' + i18n.translate('data.advancedSettings.format.currencyFormat.numeralFormatLinkText', { defaultMessage: 'numeral format', }) + @@ -509,7 +509,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.format.formattingLocaleText', values: { numeralLanguageLink: - '' + + '' + i18n.translate( 'data.advancedSettings.format.formattingLocale.numeralLanguageLinkText', { diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 5daab29348b9f..45cc95ee40804 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -8,7 +8,6 @@ export const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; export const SAMPLE_SIZE_SETTING = 'discover:sampleSize'; -export const AGGS_TERMS_SIZE_SETTING = 'discover:aggs:terms:size'; export const SORT_DEFAULT_ORDER_SETTING = 'discover:sort:defaultOrder'; export const SEARCH_ON_PAGE_LOAD_SETTING = 'discover:searchOnPageLoad'; export const DOC_HIDE_TIME_COLUMN_SETTING = 'doc_table:hideTimeColumn'; diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index e528e9708bf0d..cedc713b44f63 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -14,7 +14,6 @@ import { METRIC_TYPE } from '@kbn/analytics'; import { DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING, - AGGS_TERMS_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING, SEARCH_ON_PAGE_LOAD_SETTING, DOC_HIDE_TIME_COLUMN_SETTING, @@ -50,20 +49,6 @@ export const uiSettings: Record = { category: ['discover'], schema: schema.number(), }, - [AGGS_TERMS_SIZE_SETTING]: { - name: i18n.translate('discover.advancedSettings.aggsTermsSizeTitle', { - defaultMessage: 'Number of terms', - }), - value: 20, - type: 'number', - description: i18n.translate('discover.advancedSettings.aggsTermsSizeText', { - defaultMessage: - 'Determines how many terms will be visualized when clicking the "visualize" ' + - 'button, in the field drop downs, in the discover sidebar.', - }), - category: ['discover'], - schema: schema.number(), - }, [SORT_DEFAULT_ORDER_SETTING]: { name: i18n.translate('discover.advancedSettings.sortDefaultOrderTitle', { defaultMessage: 'Default sort direction', diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index 8ab2445b6143c..9b69dacd8fdb5 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -23,7 +23,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[eCommerce] Sales by Category', }), visState: - '{"title":"[eCommerce] Sales by Category","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Sum of total_quantity"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Sum of total_quantity","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"top","times":[],"addTimeMarker":false,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"sum","schema":"metric","params":{"field":"total_quantity"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"order_date","interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"category.keyword","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[eCommerce] Sales by Category","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Sum of total_quantity"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Sum of total_quantity","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"top","times":[],"addTimeMarker":false,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"sum","schema":"metric","params":{"field":"total_quantity"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"order_date","interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"category.keyword","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index 727ccc0bc9509..b316835029d7c 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -45,7 +45,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Flight Count and Average Ticket Price', }), visState: - '{"title":"[Flights] Flight Count and Average Ticket Price","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Average Ticket Price"}},{"id":"ValueAxis-2","name":"RightAxis-1","type":"value","position":"right","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Flight Count"}}],"seriesParams":[{"show":true,"mode":"stacked","type":"area","drawLinesBetweenPoints":true,"showCircles":false,"interpolate":"linear","lineWidth":2,"data":{"id":"5","label":"Flight Count"},"valueAxis":"ValueAxis-2"},{"show":true,"mode":"stacked","type":"line","drawLinesBetweenPoints":false,"showCircles":true,"interpolate":"linear","data":{"id":"4","label":"Average Ticket Price"},"valueAxis":"ValueAxis-1","lineWidth":2}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":13,"detailedTooltip":true},"aggs":[{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"5","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Flight Count"}},{"id":"4","enabled":true,"type":"avg","schema":"metric","params":{"field":"AvgTicketPrice","customLabel":"Average Ticket Price"}},{"id":"2","enabled":true,"type":"avg","schema":"radius","params":{"field":"AvgTicketPrice"}}]}', + '{"title":"[Flights] Flight Count and Average Ticket Price","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Average Ticket Price"}},{"id":"ValueAxis-2","name":"RightAxis-1","type":"value","position":"right","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Flight Count"}}],"seriesParams":[{"show":true,"mode":"stacked","type":"area","drawLinesBetweenPoints":true,"showCircles":false,"interpolate":"linear","lineWidth":2,"data":{"id":"5","label":"Flight Count"},"valueAxis":"ValueAxis-2"},{"show":true,"mode":"stacked","type":"line","drawLinesBetweenPoints":false,"showCircles":true,"interpolate":"linear","data":{"id":"4","label":"Average Ticket Price"},"valueAxis":"ValueAxis-1","lineWidth":2}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":13,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"5","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Flight Count"}},{"id":"4","enabled":true,"type":"avg","schema":"metric","params":{"field":"AvgTicketPrice","customLabel":"Average Ticket Price"}},{"id":"2","enabled":true,"type":"avg","schema":"radius","params":{"field":"AvgTicketPrice"}}]}', uiStateJSON: '{"vis":{"legendOpen":true,"colors":{"Average Ticket Price":"#629E51","Flight Count":"#AEA2E0"}}}', description: '', @@ -122,7 +122,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Delay Type', }), visState: - '{"title":"[Flights] Delay Type","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"FlightDelayType","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[Flights] Delay Type","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"FlightDelayType","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', uiStateJSON: '{}', description: '', version: 1, @@ -165,7 +165,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Delay Buckets', }), visState: - '{"title":"[Flights] Delay Buckets","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"histogram","schema":"segment","params":{"field":"FlightDelayMin","interval":30,"extended_bounds":{},"customLabel":"Flight Delay Minutes"}}]}', + '{"title":"[Flights] Delay Buckets","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"histogram","schema":"segment","params":{"field":"FlightDelayMin","interval":30,"extended_bounds":{},"customLabel":"Flight Delay Minutes"}}]}', uiStateJSON: '{"vis":{"legendOpen":false}}', description: '', version: 1, @@ -187,7 +187,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Flight Delays', }), visState: - '{"title":"[Flights] Flight Delays","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"BottomAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"FlightDelay","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Flight Delays"}}]}', + '{"title":"[Flights] Flight Delays","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"BottomAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"FlightDelay","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Flight Delays"}}]}', uiStateJSON: '{}', description: '', version: 1, @@ -209,7 +209,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Flight Cancellations', }), visState: - '{"title":"[Flights] Flight Cancellations","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"BottomAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"Cancelled","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Flight Cancellations"}}]}', + '{"title":"[Flights] Flight Cancellations","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"BottomAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"Cancelled","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Flight Cancellations"}}]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts index fb19545ece3a7..0396cb58d3692 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts @@ -22,7 +22,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Logs] Unique Visitors vs. Average Bytes', }), visState: - '{"title":"[Logs] Unique Visitors vs. Average Bytes","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Avg. Bytes"}},{"id":"ValueAxis-2","name":"RightAxis-1","type":"value","position":"right","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Unique Visitors"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Avg. Bytes","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"},{"show":true,"mode":"stacked","type":"line","drawLinesBetweenPoints":false,"showCircles":true,"interpolate":"linear","data":{"id":"2","label":"Unique Visitors"},"valueAxis":"ValueAxis-2"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":17,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"avg","schema":"metric","params":{"field":"bytes","customLabel":"Avg. Bytes"}},{"id":"2","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"clientip","customLabel":"Unique Visitors"}},{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"4","enabled":true,"type":"count","schema":"radius","params":{}}]}', + '{"title":"[Logs] Unique Visitors vs. Average Bytes","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Avg. Bytes"}},{"id":"ValueAxis-2","name":"RightAxis-1","type":"value","position":"right","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Unique Visitors"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Avg. Bytes","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"},{"show":true,"mode":"stacked","type":"line","drawLinesBetweenPoints":false,"showCircles":true,"interpolate":"linear","data":{"id":"2","label":"Unique Visitors"},"valueAxis":"ValueAxis-2"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":17,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"avg","schema":"metric","params":{"field":"bytes","customLabel":"Avg. Bytes"}},{"id":"2","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"clientip","customLabel":"Unique Visitors"}},{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"4","enabled":true,"type":"count","schema":"radius","params":{}}]}', uiStateJSON: '{"vis":{"colors":{"Avg. Bytes":"#70DBED","Unique Visitors":"#0A437C"}}}', description: '', version: 1, diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx index d15445f3e10ae..29945e15874b7 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -91,7 +91,10 @@ export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxEr const painlessSyntaxErrors = PainlessLang.getSyntaxErrors(); // It is possible for there to be more than one editor in a view, // so we need to get the syntax errors based on the editor (aka model) ID - const editorHasSyntaxErrors = editorId && painlessSyntaxErrors[editorId].length > 0; + const editorHasSyntaxErrors = + editorId && + painlessSyntaxErrors[editorId] && + painlessSyntaxErrors[editorId].length > 0; if (editorHasSyntaxErrors) { return resolve({ diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap index 0f35267e1fb38..2ab8037639f85 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap @@ -11,7 +11,7 @@ exports[`BytesFormatEditor should render normally 1`] = ` helpText={ ; const fieldType = 'number'; const format = { @@ -25,7 +29,20 @@ const formatParams = { const onChange = jest.fn(); const onError = jest.fn(); +const KibanaReactContext = createKibanaReactContext( + coreMock.createStart({ basePath: 'my-base-path' }) +); + describe('BytesFormatEditor', () => { + beforeAll(() => { + // Enzyme does not support the new Context API in shallow rendering. + // @see https://github.com/enzymejs/enzyme/issues/2189 + (BytesFormatEditor as React.ComponentType).contextTypes = { + services: () => null, + }; + delete (BytesFormatEditor as Partial).contextType; + }); + it('should have a formatId', () => { expect(BytesFormatEditor.formatId).toEqual('bytes'); }); @@ -38,7 +55,8 @@ describe('BytesFormatEditor', () => { formatParams={formatParams} onChange={onChange} onError={onError} - /> + />, + { context: KibanaReactContext.value } ); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap index 3cac385054835..4d42e3848d3cd 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap @@ -11,7 +11,7 @@ exports[`NumberFormatEditor should render normally 1`] = ` helpText={ ; + const fieldType = 'number'; const format = { getConverterFor: jest.fn().mockImplementation(() => (input: number) => input * 2), @@ -25,7 +29,20 @@ const formatParams = { const onChange = jest.fn(); const onError = jest.fn(); +const KibanaReactContext = createKibanaReactContext( + coreMock.createStart({ basePath: 'my-base-path' }) +); + describe('NumberFormatEditor', () => { + beforeAll(() => { + // Enzyme does not support the new Context API in shallow rendering. + // @see https://github.com/enzymejs/enzyme/issues/2189 + (NumberFormatEditor as React.ComponentType).contextTypes = { + services: () => null, + }; + delete (NumberFormatEditor as Partial).contextType; + }); + it('should have a formatId', () => { expect(NumberFormatEditor.formatId).toEqual('number'); }); @@ -38,7 +55,8 @@ describe('NumberFormatEditor', () => { formatParams={formatParams} onChange={onChange} onError={onError} - /> + />, + { context: KibanaReactContext.value } ); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx index 2aeb90373bfab..f55c0c5f06c12 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx @@ -15,12 +15,17 @@ import { DefaultFormatEditor, defaultState } from '../default'; import { FormatEditorSamples } from '../../samples'; +import { context as contextType } from '../../../../../../kibana_react/public'; + export interface NumberFormatEditorParams { pattern: string; } export class NumberFormatEditor extends DefaultFormatEditor { + static contextType = contextType; static formatId = 'number'; + + context!: React.ContextType; state = { ...defaultState, sampleInputs: [10000, 12.345678, -1, -999, 0.52], @@ -43,7 +48,10 @@ export class NumberFormatEditor extends DefaultFormatEditor - + ; + const fieldType = 'number'; const format = { getConverterFor: jest.fn().mockImplementation(() => (input: number) => input * 2), @@ -25,7 +29,20 @@ const formatParams = { const onChange = jest.fn(); const onError = jest.fn(); +const KibanaReactContext = createKibanaReactContext( + coreMock.createStart({ basePath: 'my-base-path' }) +); + describe('PercentFormatEditor', () => { + beforeAll(() => { + // Enzyme does not support the new Context API in shallow rendering. + // @see https://github.com/enzymejs/enzyme/issues/2189 + (PercentFormatEditor as React.ComponentType).contextTypes = { + services: () => null, + }; + delete (PercentFormatEditor as Partial).contextType; + }); + it('should have a formatId', () => { expect(PercentFormatEditor.formatId).toEqual('percent'); }); @@ -38,7 +55,8 @@ describe('PercentFormatEditor', () => { formatParams={formatParams} onChange={onChange} onError={onError} - /> + />, + { context: KibanaReactContext.value } ); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap index ed1183ac50532..bd6af083675ea 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -1,5 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Table render name 1`] = ` + + customer + +`; + +exports[`Table render name 2`] = ` + + customer + +   + + This field exists on the index pattern only. + + } + title="Runtime field" + type="indexSettings" + /> + + +`; + exports[`Table should render conflicting type 1`] = ` conflict @@ -142,6 +166,15 @@ exports[`Table should render normally 1`] = ` "name": "conflictingField", "type": "text, long", }, + Object { + "displayName": "customer", + "excluded": false, + "info": Array [], + "isMapped": false, + "kbnType": "text", + "name": "customer", + "type": "keyword", + }, ] } pagination={ diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx index 9c154ce1b0e7b..76ffe61234e34 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { IIndexPattern } from 'src/plugins/data/public'; import { IndexedFieldItem } from '../../types'; -import { Table } from './table'; +import { Table, renderFieldName } from './table'; const indexPattern = { timeFieldName: 'timestamp', @@ -48,6 +48,15 @@ const items: IndexedFieldItem[] = [ format: '', isMapped: true, }, + { + name: 'customer', + displayName: 'customer', + type: 'keyword', + kbnType: 'text', + info: [], + excluded: false, + isMapped: false, + }, ]; const renderTable = ( @@ -103,4 +112,28 @@ describe('Table', () => { renderTable({ editField }).prop('columns')[6].actions[0].onClick(); expect(editField).toBeCalled(); }); + + test('render name', () => { + const mappedField = { + name: 'customer', + info: [], + excluded: false, + kbnType: 'string', + type: 'keyword', + isMapped: true, + }; + + expect(renderFieldName(mappedField)).toMatchSnapshot(); + + const runtimeField = { + name: 'customer', + info: [], + excluded: false, + kbnType: 'string', + type: 'keyword', + isMapped: false, + }; + + expect(renderFieldName(runtimeField)).toMatchSnapshot(); + }); }); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index 4e9a2bb645112..2b8a18cc67c64 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -157,6 +157,16 @@ const labelDescription = i18n.translate( { defaultMessage: 'A custom label for the field.' } ); +const runtimeIconTipTitle = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.runtimeIconTipTitle', + { defaultMessage: 'Runtime field' } +); + +const runtimeIconTipText = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.runtimeIconTipText', + { defaultMessage: 'This field exists on the index pattern only.' } +); + interface IndexedFieldProps { indexPattern: IIndexPattern; items: IndexedFieldItem[]; @@ -164,54 +174,60 @@ interface IndexedFieldProps { deleteField: (fieldName: string) => void; } +export const renderFieldName = (field: IndexedFieldItem, timeFieldName?: string) => ( + + {field.name} + {field.info && field.info.length ? ( + +   + ( +
{info}
+ ))} + /> +
+ ) : null} + {timeFieldName === field.name ? ( + +   + + + ) : null} + {!field.isMapped ? ( + +   + {runtimeIconTipText}} + /> +
+ ) : null} + {field.customLabel && field.customLabel !== field.name ? ( +
+ + + {field.customLabel} + + +
+ ) : null} +
+); + export class Table extends PureComponent { renderBooleanTemplate(value: string, arialLabel: string) { return value ? : ; } - renderFieldName(name: string, field: IndexedFieldItem) { - const { indexPattern } = this.props; - - return ( - - {field.name} - {field.info && field.info.length ? ( - -   - ( -
{info}
- ))} - /> -
- ) : null} - {indexPattern.timeFieldName === name ? ( - -   - - - ) : null} - {field.customLabel && field.customLabel !== field.name ? ( -
- - - {field.customLabel} - - -
- ) : null} -
- ); - } - renderFieldType(type: string, isConflict: boolean) { return ( @@ -234,7 +250,7 @@ export class Table extends PureComponent { } render() { - const { items, editField, deleteField } = this.props; + const { items, editField, deleteField, indexPattern } = this.props; const pagination = { initialPageSize: 10, @@ -248,7 +264,7 @@ export class Table extends PureComponent { dataType: 'string', sortable: true, render: (value: string, field: IndexedFieldItem) => { - return this.renderFieldName(value, field); + return renderFieldName(field, indexPattern.timeFieldName); }, width: '38%', 'data-test-subj': 'indexedFieldName', diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index f166e4fcebfa3..b8100c048d512 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -196,10 +196,6 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'long', _meta: { description: 'Non-default value of setting.' }, }, - 'discover:aggs:terms:size': { - type: 'long', - _meta: { description: 'Non-default value of setting.' }, - }, 'context:tieBreakerFields': { type: 'array', items: { 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 8bbc14e0678d3..15d78e3e79b0e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -68,7 +68,6 @@ export interface UsageStats { 'discover:sampleSize': number; defaultColumns: string[]; 'context:defaultSize': number; - 'discover:aggs:terms:size': number; 'context:tieBreakerFields': string[]; 'discover:sort:defaultOrder': string; 'context:step': number; 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 33c037c2e84b3..57d05262319f2 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 @@ -30,7 +30,7 @@ export interface SaveModalDashboardProps { documentInfo: SaveModalDocumentInfo; objectType: string; onClose: () => void; - onSave: (props: OnSaveProps & { dashboardId: string | null }) => void; + onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); } @@ -48,6 +48,9 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>( documentId || disableDashboardOptions ? null : 'existing' ); + const [isAddToLibrarySelected, setAddToLibrary] = useState( + !initialCopyOnSave || disableDashboardOptions + ); const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>( null ); @@ -62,12 +65,13 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { onChange={(option) => { setDashboardOption(option); }} - {...{ copyOnSave, documentId, dashboardOption }} + {...{ copyOnSave, documentId, dashboardOption, setAddToLibrary, isAddToLibrarySelected }} /> ) : null; const onCopyOnSaveChange = (newCopyOnSave: boolean) => { + setAddToLibrary(true); setDashboardOption(null); setCopyOnSave(newCopyOnSave); }; @@ -85,7 +89,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { } } - props.onSave({ ...onSaveProps, dashboardId }); + props.onSave({ ...onSaveProps, dashboardId, addToLibrary: isAddToLibrarySelected }); }; const saveLibraryLabel = @@ -113,7 +117,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { onSave={onModalSave} title={documentInfo.title} showCopyOnSave={documentId ? true : false} - options={dashboardOption === null ? tagOptions : undefined} // Show tags when not adding to dashboard + options={isAddToLibrarySelected ? tagOptions : undefined} // Show tags when not adding to dashboard description={documentInfo.description} showDescription={true} {...{ diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx index 6b41058f9c6c5..dd6fd975f8e07 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx @@ -44,6 +44,7 @@ export function Example({ hasDocumentId: boolean; } & StorybookParams) { const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>('existing'); + const [isAddToLibrarySelected, setAddToLibrary] = useState(false); return ( ); } 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 c2b5eac4dbb83..1ae54040571a2 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 @@ -19,6 +19,7 @@ import { EuiIconTip, EuiPanel, EuiSpacer, + EuiCheckbox, } from '@elastic/eui'; import { DashboardPicker, DashboardPickerProps } from './dashboard_picker'; @@ -29,24 +30,105 @@ export interface SaveModalDashboardSelectorProps { copyOnSave: boolean; documentId?: string; onSelectDashboard: DashboardPickerProps['onChange']; - + setAddToLibrary: (selected: boolean) => void; + isAddToLibrarySelected: boolean; dashboardOption: 'new' | 'existing' | null; onChange: (dashboardOption: 'new' | 'existing' | null) => void; } export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProps) { - const { documentId, onSelectDashboard, dashboardOption, onChange, copyOnSave } = props; + const { + documentId, + onSelectDashboard, + setAddToLibrary, + isAddToLibrarySelected, + dashboardOption, + onChange, + copyOnSave, + } = props; const isDisabled = !copyOnSave && !!documentId; return ( <> + } + hasChildLabel={false} + > + <> + +
+ <> + onChange('existing')} + disabled={isDisabled} + /> +
+ +
+ + + <> + onChange('new')} + disabled={isDisabled} + /> + + + { + setAddToLibrary(true); + onChange(null); + }} + disabled={isDisabled} + /> +
+
+ - - + setAddToLibrary(event.target.checked)} /> @@ -55,67 +137,13 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp content={ } /> - } - hasChildLabel={false} - > - -
- <> - onChange('existing')} - disabled={isDisabled} - /> -
- -
- - - <> - onChange('new')} - disabled={isDisabled} - /> - - - onChange(null)} - disabled={isDisabled} - /> -
-
+
); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index f7795dbf9b2f8..cadec303d6e6e 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7703,12 +7703,6 @@ "description": "Non-default value of setting." } }, - "discover:aggs:terms:size": { - "type": "long", - "_meta": { - "description": "Non-default value of setting." - } - }, "context:tieBreakerFields": { "type": "array", "items": { diff --git a/src/plugins/usage_collection/README.mdx b/src/plugins/usage_collection/README.mdx index 9516ff656f460..e6759b7dc6c7c 100644 --- a/src/plugins/usage_collection/README.mdx +++ b/src/plugins/usage_collection/README.mdx @@ -166,7 +166,10 @@ In many cases, plugins need to report the custom usage of a feature. In this cas type: 'MY_USAGE_TYPE', schema: { my_objects: { - total: 'long', + total: { + type: 'long', + _meta: { description: 'The total number of objects in the cluster created in the last 24h' }, + }, }, }, isReady: () => isCollectorFetchReady, // Method to return `true`/`false` or Promise(`true`/`false`) to confirm if the collector is ready for the `fetch` method to be called. @@ -211,7 +214,7 @@ schema: { my_greeting: { type: 'keyword', _meta: { - description: 'The greeting keyword', + description: 'The greeting shown to the user. It reports only when overwritten by the user.', } } } @@ -254,21 +257,27 @@ export const myCollector = makeUsageCollector({ schema: { my_greeting: { type: 'keyword', + _meta: { description: 'The greeting shown to the user. It reports only when overwritten by the user.' } }, some_obj: { total: { type: 'long', + _meta: { description: 'The total count of some_obj since the creation of the cluster' } }, }, some_array: { type: 'array', - items: { type: 'keyword' } + items: { + type: 'keyword', + _meta: { description: 'Category assigned to ...' } + } }, some_array_of_obj: { type: 'array', items: { total: { type: 'long', + _meta: { description: 'The daily total number of items.' } }, }, }, diff --git a/src/plugins/visualizations/public/embeddable/to_ast.ts b/src/plugins/visualizations/public/embeddable/to_ast.ts index 5436b78c1b71f..7ccff9394943a 100644 --- a/src/plugins/visualizations/public/embeddable/to_ast.ts +++ b/src/plugins/visualizations/public/embeddable/to_ast.ts @@ -10,6 +10,7 @@ import { ExpressionFunctionKibana, ExpressionFunctionKibanaContext } from '../.. import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; import { VisToExpressionAst } from '../types'; +import { queryToAst } from '../../../data/common'; /** * Creates an ast expression for a visualization based on kibana context (query, filters, timerange) @@ -25,7 +26,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params) => { const kibana = buildExpressionFunction('kibana', {}); const kibanaContext = buildExpressionFunction('kibana_context', { - q: query && JSON.stringify(query), + q: query && queryToAst(query), filters: filters && JSON.stringify(filters), savedSearchId, }); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts index 5c74de3e2ef9a..6f1fba26b39b3 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -1672,7 +1672,12 @@ describe('migration visualization', () => { doc as Parameters[0], savedObjectMigrationContext ); - const getTestDoc = (type = 'area', categoryAxes?: object[], valueAxes?: object[]) => ({ + const getTestDoc = ( + type = 'area', + categoryAxes?: object[], + valueAxes?: object[], + hasPalette = false + ) => ({ attributes: { title: 'My Vis', description: 'This is my super cool vis.', @@ -1691,6 +1696,12 @@ describe('migration visualization', () => { labels: {}, }, ], + ...(hasPalette && { + palette: { + type: 'palette', + name: 'default', + }, + }), }, }), }, @@ -1709,13 +1720,20 @@ describe('migration visualization', () => { expect(isVislibVis).toEqual(true); }); - it('should decorate existing docs with the kibana legacy palette', () => { + it('should decorate existing docs without a predefined palette with the kibana legacy palette', () => { const migratedTestDoc = migrate(getTestDoc()); const { palette } = JSON.parse(migratedTestDoc.attributes.visState).params; expect(palette.name).toEqual('kibana_palette'); }); + it('should not overwrite the palette with the legacy one if the palette already exists in the saved object', () => { + const migratedTestDoc = migrate(getTestDoc('area', undefined, undefined, true)); + const { palette } = JSON.parse(migratedTestDoc.attributes.visState).params; + + expect(palette.name).toEqual('default'); + }); + describe('labels.filter', () => { it('should keep existing categoryAxes labels.filter value', () => { const migratedTestDoc = migrate(getTestDoc('area', [{ labels: { filter: false } }])); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index 2c72208b8b283..afb59266d0dbf 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -859,6 +859,7 @@ const migrateVislibAreaLineBarTypes: SavedObjectMigrationFn = (doc) => const isHorizontalBar = visState.type === 'horizontal_bar'; const isLineOrArea = visState?.params?.type === CHART_TYPE_AREA || visState?.params?.type === CHART_TYPE_LINE; + const hasPalette = visState?.params?.palette; return { ...doc, attributes: { @@ -867,10 +868,12 @@ const migrateVislibAreaLineBarTypes: SavedObjectMigrationFn = (doc) => ...visState, params: { ...visState.params, - palette: { - type: 'palette', - name: 'kibana_palette', - }, + ...(!hasPalette && { + palette: { + type: 'palette', + name: 'kibana_palette', + }, + }), categoryAxes: visState.params.categoryAxes && decorateAxes(visState.params.categoryAxes, !isHorizontalBar), diff --git a/src/plugins/visualize/common/constants.ts b/src/plugins/visualize/common/constants.ts index fcdc7c1cbc9a2..5fe8ed7e095a2 100644 --- a/src/plugins/visualize/common/constants.ts +++ b/src/plugins/visualize/common/constants.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -export const AGGS_TERMS_SIZE_SETTING = 'discover:aggs:terms:size'; export const STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index e8c3289d4ce41..4f5679a14b0b7 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -93,7 +93,7 @@ export const getTopNavConfig = ( /** * Called when the user clicks "Save" button. */ - async function doSave(saveOptions: SavedObjectSaveOpts) { + async function doSave(saveOptions: SavedObjectSaveOpts & { dashboardId?: string }) { const newlyCreated = !Boolean(savedVis.id) || savedVis.copyOnSave; // vis.title was not bound and it's needed to reflect title into visState stateContainer.transitions.setVis({ @@ -118,7 +118,7 @@ export const getTopNavConfig = ( 'data-test-subj': 'saveVisualizationSuccess', }); - if (originatingApp && saveOptions.returnToOrigin) { + if ((originatingApp && saveOptions.returnToOrigin) || saveOptions.dashboardId) { if (!embeddableId) { const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(id)}`; @@ -127,16 +127,26 @@ export const getTopNavConfig = ( setActiveUrl(appPath); } + const app = originatingApp || 'dashboards'; + + let path; + if (saveOptions.dashboardId) { + path = + saveOptions.dashboardId === 'new' ? '#/create' : `#/view/${saveOptions.dashboardId}`; + } + if (newlyCreated && stateTransfer) { - stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { + stateTransfer.navigateToWithEmbeddablePackage(app, { state: { type: VISUALIZE_EMBEDDABLE_TYPE, input: { savedObjectId: id }, embeddableId, }, + path, }); } else { - application.navigateToApp(originatingApp); + // TODO: need the same thing here? + application.navigateToApp(app, { path }); } } else { if (setOriginatingApp && originatingApp && newlyCreated) { @@ -321,7 +331,11 @@ export const getTopNavConfig = ( newDescription, returnToOrigin, dashboardId, - }: OnSaveProps & { returnToOrigin?: boolean } & { dashboardId?: string | null }) => { + addToLibrary, + }: OnSaveProps & { returnToOrigin?: boolean } & { + dashboardId?: string | null; + addToLibrary?: boolean; + }) => { const currentTitle = savedVis.title; savedVis.title = newTitle; embeddableHandler.updateInput({ title: newTitle }); @@ -337,9 +351,12 @@ export const getTopNavConfig = ( isTitleDuplicateConfirmed, onTitleDuplicate, returnToOrigin, + dashboardId: !!dashboardId ? dashboardId : undefined, }; - if (dashboardId) { + // If we're adding to a dashboard and not saving to library, + // we'll want to use a by-value operation + if (dashboardId && !addToLibrary) { const appPath = `${VisualizeConstants.LANDING_PAGE_PATH}`; // Manually insert a new url so the back button will open the saved visualization. @@ -369,6 +386,8 @@ export const getTopNavConfig = ( return { id: true }; } + // We're adding the viz to a library so we need to save it and then + // add to a dashboard if necessary const response = await doSave(saveOptions); // If the save wasn't successful, put the original values back. if (!response.id || response.error) { diff --git a/test/api_integration/apis/telemetry/utils/schema_to_config_schema.test.js b/test/api_integration/apis/telemetry/utils/schema_to_config_schema.test.js index f568a4338ebe5..2ae4e1723cc25 100644 --- a/test/api_integration/apis/telemetry/utils/schema_to_config_schema.test.js +++ b/test/api_integration/apis/telemetry/utils/schema_to_config_schema.test.js @@ -151,5 +151,23 @@ describe(`assertTelemetryPayload`, () => { { im_only_passing_through_data: [{ docs: { field: 1 } }] } ) ).not.toThrow(); + + // Even when properties exist + expect(() => + assertTelemetryPayload( + { + root: { + properties: { + im_only_passing_through_data: { + type: 'pass_through', + properties: {}, + }, + }, + }, + plugins: { properties: {} }, + }, + { im_only_passing_through_data: [{ docs: { field: 1 } }] } + ) + ).not.toThrow(); }); }); diff --git a/test/api_integration/apis/telemetry/utils/schema_to_config_schema.ts b/test/api_integration/apis/telemetry/utils/schema_to_config_schema.ts index d5b18eb4bd202..b45930682e3aa 100644 --- a/test/api_integration/apis/telemetry/utils/schema_to_config_schema.ts +++ b/test/api_integration/apis/telemetry/utils/schema_to_config_schema.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { schema, ObjectType, Type } from '@kbn/config-schema'; +import type { ObjectType, Type } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { get } from 'lodash'; import { set } from '@elastic/safer-lodash-set'; import type { AllowedSchemaTypes } from 'src/plugins/usage_collection/server'; @@ -38,6 +39,11 @@ function isOneOfCandidate( * @param value */ function valueSchemaToConfigSchema(value: TelemetrySchemaValue): Type { + // We need to check the pass_through type on top of everything + if ((value as { type: 'pass_through' }).type === 'pass_through') { + return schema.any(); + } + if ('properties' in value) { const { DYNAMIC_KEY, ...properties } = value.properties; const schemas: Array> = [objectSchemaToConfigSchema({ properties })]; @@ -48,8 +54,6 @@ function valueSchemaToConfigSchema(value: TelemetrySchemaValue): Type { } else { const valueType = value.type; // Copied in here because of TS reasons, it's not available in the `default` case switch (value.type) { - case 'pass_through': - return schema.any(); case 'boolean': return schema.boolean(); case 'keyword': @@ -77,9 +81,11 @@ function valueSchemaToConfigSchema(value: TelemetrySchemaValue): Type { } function objectSchemaToConfigSchema(objectSchema: TelemetrySchemaObject): ObjectType { + const objectEntries = Object.entries(objectSchema.properties); + return schema.object( Object.fromEntries( - Object.entries(objectSchema.properties).map(([key, value]) => { + objectEntries.map(([key, value]) => { try { return [key, schema.maybe(valueSchemaToConfigSchema(value))]; } catch (err) { diff --git a/test/functional/apps/dashboard/dashboard_saved_query.ts b/test/functional/apps/dashboard/dashboard_saved_query.ts index 307c34d3f3c43..bdf97e8ced140 100644 --- a/test/functional/apps/dashboard/dashboard_saved_query.ts +++ b/test/functional/apps/dashboard/dashboard_saved_query.ts @@ -38,7 +38,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await savedQueryManagementComponent.openSavedQueryManagementComponent(); const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); expect(descriptionText).to.eql( - 'SAVED QUERIES\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' + 'Saved Queries\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' ); }); diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index ddd1c4648a0b2..23f3af37bbdf6 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -55,7 +55,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await savedQueryManagementComponent.openSavedQueryManagementComponent(); const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); expect(descriptionText).to.eql( - 'SAVED QUERIES\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' + 'Saved Queries\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' ); }); diff --git a/test/functional/apps/visualize/_add_to_dashboard.ts b/test/functional/apps/visualize/_add_to_dashboard.ts index 1d1bd62988f45..17d628db86d25 100644 --- a/test/functional/apps/visualize/_add_to_dashboard.ts +++ b/test/functional/apps/visualize/_add_to_dashboard.ts @@ -26,7 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); describe('Add to Dashboard', function describeIndexTests() { - it('adding a new metric to a new dashboard', async function () { + it('adding a new metric to a new dashboard by value', async function () { await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickMetric(); await PageObjects.visualize.clickNewSearch(); @@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timeToVisualize.saveFromModal('My New Vis 1', { addToDashboard: 'new', + saveToLibrary: false, }); await PageObjects.dashboard.waitForRenderComplete(); @@ -43,10 +44,39 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(1); + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists('My New Vis 1'); + expect(isLinked).to.be(false); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('adding a new metric to a new dashboard by reference', async function () { + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickMetric(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + await testSubjects.click('visualizeSaveButton'); + + await PageObjects.timeToVisualize.saveFromModal('My Saved New Vis 1', { + addToDashboard: 'new', + saveToLibrary: true, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['14,004']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'My Saved New Vis 1' + ); + expect(isLinked).to.be(true); + await PageObjects.timeToVisualize.resetNewDashboard(); }); - it('adding a existing metric to a new dashboard', async function () { + it('adding a existing metric to a new dashboard by value', async function () { await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickMetric(); await PageObjects.visualize.clickNewSearch(); @@ -57,6 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Save this new viz to library await PageObjects.timeToVisualize.saveFromModal('My New Vis 1', { addToDashboard: null, + saveToLibrary: true, }); await testSubjects.click('visualizeSaveButton'); @@ -68,6 +99,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timeToVisualize.saveFromModal('My New Vis 1 Copy', { addToDashboard: 'new', saveAsNew: true, + saveToLibrary: false, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['14,004']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'My New Vis 1 Copy' + ); + expect(isLinked).to.be(false); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('adding a existing metric to a new dashboard by reference', async function () { + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickMetric(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + await testSubjects.click('visualizeSaveButton'); + + // Save this new viz to library + await PageObjects.timeToVisualize.saveFromModal('Another New Vis 1', { + addToDashboard: null, + saveToLibrary: true, + }); + + await testSubjects.click('visualizeSaveButton'); + + // All the options should be disabled + await PageObjects.timeToVisualize.ensureDashboardOptionsAreDisabled(); + + // Save a new copy of this viz to a new dashboard + await PageObjects.timeToVisualize.saveFromModal('Another New Vis 1 Copy', { + addToDashboard: 'new', + saveAsNew: true, + saveToLibrary: true, }); await PageObjects.dashboard.waitForRenderComplete(); @@ -75,10 +146,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(1); + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'Another New Vis 1 Copy' + ); + expect(isLinked).to.be(true); + await PageObjects.timeToVisualize.resetNewDashboard(); }); - it('adding a new metric to an existing dashboard', async function () { + it('adding a new metric to an existing dashboard by value', async function () { + await PageObjects.common.navigateToApp('dashboard'); + + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.addVisualizations(['Visualization AreaChart']); + await PageObjects.dashboard.saveDashboard('My Excellent Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Excellent Dashboard', 1); + + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickMetric(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + await testSubjects.click('visualizeSaveButton'); + + await PageObjects.timeToVisualize.saveFromModal('My New Vis 2', { + addToDashboard: 'existing', + dashboardId: 'My Excellent Dashboard', + saveToLibrary: false, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['14,004']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists('My New Vis 2'); + expect(isLinked).to.be(false); + }); + + it('adding a new metric to an existing dashboard by reference', async function () { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); @@ -94,18 +201,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('visualizeSaveButton'); - await PageObjects.timeToVisualize.saveFromModal('My New Vis 2', { + await PageObjects.timeToVisualize.saveFromModal('My Saved New Vis 2', { addToDashboard: 'existing', dashboardId: 'My Wonderful Dashboard', + saveToLibrary: true, }); await PageObjects.dashboard.waitForRenderComplete(); await dashboardExpect.metricValuesExist(['14,004']); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(2); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'My Saved New Vis 2' + ); + expect(isLinked).to.be(true); }); - it('adding a existing metric to an existing dashboard', async function () { + it('adding a existing metric to an existing dashboard by value', async function () { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); @@ -124,6 +237,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Save this new viz to library await PageObjects.timeToVisualize.saveFromModal('My New Vis 2', { addToDashboard: null, + saveToLibrary: true, }); await testSubjects.click('visualizeSaveButton'); @@ -136,12 +250,64 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { addToDashboard: 'existing', dashboardId: 'My Very Cool Dashboard', saveAsNew: true, + saveToLibrary: false, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['14,004']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'My New Vis 2 Copy' + ); + expect(isLinked).to.be(false); + }); + + it('adding a existing metric to an existing dashboard by reference', async function () { + await PageObjects.common.navigateToApp('dashboard'); + + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.addVisualizations(['Visualization AreaChart']); + await PageObjects.dashboard.saveDashboard('My Very Neat Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Very Neat Dashboard', 1); + + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickMetric(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + await testSubjects.click('visualizeSaveButton'); + + // Save this new viz to library + await PageObjects.timeToVisualize.saveFromModal('Neat Saved Vis 2', { + addToDashboard: null, + saveToLibrary: true, + }); + + await testSubjects.click('visualizeSaveButton'); + + // All the options should be disabled + await PageObjects.timeToVisualize.ensureDashboardOptionsAreDisabled(); + + // Save a new copy of this viz to an existing dashboard + await PageObjects.timeToVisualize.saveFromModal('Neat Saved Vis 2 Copy', { + addToDashboard: 'existing', + dashboardId: 'My Very Neat Dashboard', + saveAsNew: true, + saveToLibrary: true, }); await PageObjects.dashboard.waitForRenderComplete(); await dashboardExpect.metricValuesExist(['14,004']); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(2); + + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'Neat Saved Vis 2 Copy' + ); + expect(isLinked).to.be(true); }); }); } diff --git a/test/functional/page_objects/time_to_visualize_page.ts b/test/functional/page_objects/time_to_visualize_page.ts index 560f73cbcdbd8..458b4dd3e60a1 100644 --- a/test/functional/page_objects/time_to_visualize_page.ts +++ b/test/functional/page_objects/time_to_visualize_page.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; interface SaveModalArgs { addToDashboard?: 'new' | 'existing' | null; + saveToLibrary?: boolean; dashboardId?: string; saveAsNew?: boolean; redirectToOrigin?: boolean; @@ -35,7 +36,9 @@ export function TimeToVisualizePageProvider({ getService, getPageObjects }: FtrP const dashboardSelector = await testSubjects.find('add-to-dashboard-options'); await dashboardSelector.findByCssSelector(`input[id="new-dashboard-option"]:disabled`); await dashboardSelector.findByCssSelector(`input[id="existing-dashboard-option"]:disabled`); - await dashboardSelector.findByCssSelector(`input[id="add-to-library-option"]:disabled`); + + const librarySelector = await testSubjects.find('add-to-library-checkbox'); + await librarySelector.findByCssSelector(`input[id="add-to-library-checkbox"]:disabled`); } public async resetNewDashboard() { @@ -46,7 +49,13 @@ export function TimeToVisualizePageProvider({ getService, getPageObjects }: FtrP public async setSaveModalValues( vizName: string, - { saveAsNew, redirectToOrigin, addToDashboard, dashboardId }: SaveModalArgs = {} + { + saveAsNew, + redirectToOrigin, + addToDashboard, + dashboardId, + saveToLibrary, + }: SaveModalArgs = {} ) { await testSubjects.setValue('savedObjectTitle', vizName); @@ -57,13 +66,6 @@ export function TimeToVisualizePageProvider({ getService, getPageObjects }: FtrP await testSubjects.setEuiSwitch('saveAsNewCheckbox', state); } - const hasRedirectToOrigin = await testSubjects.exists('returnToOriginModeSwitch'); - if (hasRedirectToOrigin && redirectToOrigin !== undefined) { - const state = redirectToOrigin ? 'check' : 'uncheck'; - log.debug('redirect to origin checkbox exists. Setting its state to', state); - await testSubjects.setEuiSwitch('returnToOriginModeSwitch', state); - } - const hasDashboardSelector = await testSubjects.exists('add-to-dashboard-options'); if (hasDashboardSelector && addToDashboard !== undefined) { let option: DashboardPickerOption = 'add-to-library-option'; @@ -80,6 +82,40 @@ export function TimeToVisualizePageProvider({ getService, getPageObjects }: FtrP await find.clickByButtonText(dashboardId); } } + + const hasSaveToLibrary = await testSubjects.exists('add-to-library-checkbox'); + if (hasSaveToLibrary && saveToLibrary !== undefined) { + const libraryCheckbox = await find.byCssSelector('#add-to-library-checkbox'); + const isChecked = await libraryCheckbox.isSelected(); + const needsClick = isChecked !== saveToLibrary; + const state = saveToLibrary ? 'check' : 'uncheck'; + + log.debug('save to library checkbox exists. Setting its state to', state); + if (needsClick) { + const selector = await testSubjects.find('add-to-library-checkbox'); + const label = await selector.findByCssSelector(`label[for="add-to-library-checkbox"]`); + await label.click(); + } + } + + const hasRedirectToOrigin = await testSubjects.exists('returnToOriginModeSwitch'); + if (hasRedirectToOrigin && redirectToOrigin !== undefined) { + const state = redirectToOrigin ? 'check' : 'uncheck'; + log.debug('redirect to origin checkbox exists. Setting its state to', state); + await testSubjects.setEuiSwitch('returnToOriginModeSwitch', state); + } + } + + public async libraryNotificationExists(panelTitle: string) { + log.debug('searching for library modal on panel:', panelTitle); + const panel = await testSubjects.find( + `embeddablePanelHeading-${panelTitle.replace(/ /g, '')}` + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + panel + ); + return libraryActionExists; } public async saveFromModal( diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 963a6bff0cd0b..d7bb84394ae3c 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -605,7 +605,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro await comboBox.setElement(groupBy, 'Terms', { clickWithMouse: true }); await PageObjects.common.sleep(1000); const byField = await testSubjects.find('groupByField'); - await comboBox.setElement(byField, field, { clickWithMouse: true }); + await comboBox.setElement(byField, field); } public async checkSelectedMetricsGroupByValue(value: string) { diff --git a/test/functional/screenshots/baseline/area_chart.png b/test/functional/screenshots/baseline/area_chart.png index 8ff0df0563eda..e32dbbaf8d1af 100644 Binary files a/test/functional/screenshots/baseline/area_chart.png and b/test/functional/screenshots/baseline/area_chart.png differ diff --git a/test/functional/screenshots/baseline/tsvb_dashboard.png b/test/functional/screenshots/baseline/tsvb_dashboard.png index b0264ce8d4592..e0d79c7234f6a 100644 Binary files a/test/functional/screenshots/baseline/tsvb_dashboard.png and b/test/functional/screenshots/baseline/tsvb_dashboard.png differ diff --git a/test/functional/services/field_editor.ts b/test/functional/services/field_editor.ts new file mode 100644 index 0000000000000..5cd1f2c4f6202 --- /dev/null +++ b/test/functional/services/field_editor.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 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 { FtrProviderContext } from '../ftr_provider_context'; + +export function FieldEditorProvider({ getService }: FtrProviderContext) { + const browser = getService('browser'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + + class FieldEditor { + public async setName(name: string) { + await testSubjects.setValue('nameField > input', name); + } + public async enableValue() { + await testSubjects.setEuiSwitch('valueRow > toggle', 'check'); + } + public async disableValue() { + await testSubjects.setEuiSwitch('valueRow > toggle', 'uncheck'); + } + public async typeScript(script: string) { + const editor = await (await testSubjects.find('valueRow')).findByClassName( + 'react-monaco-editor-container' + ); + const textarea = await editor.findByClassName('monaco-mouse-cursor-text'); + + await textarea.click(); + await browser.pressKeys(script); + } + public async save() { + await retry.try(async () => { + await testSubjects.click('fieldSaveButton'); + await testSubjects.missingOrFail('fieldSaveButton', { timeout: 2000 }); + }); + } + } + + return new FieldEditor(); +} diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 07d5ef950d21e..0dd7f20debcbd 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -31,6 +31,7 @@ import { FilterBarProvider } from './filter_bar'; import { FlyoutProvider } from './flyout'; import { GlobalNavProvider } from './global_nav'; import { InspectorProvider } from './inspector'; +import { FieldEditorProvider } from './field_editor'; import { ManagementMenuProvider } from './management'; import { QueryBarProvider } from './query_bar'; import { RemoteProvider } from './remote'; @@ -74,6 +75,7 @@ export const services = { browser: BrowserProvider, pieChart: PieChartProvider, inspector: InspectorProvider, + fieldEditor: FieldEditorProvider, vegaDebugInspector: VegaDebugInspectorViewProvider, appsMenu: AppsMenuProvider, globalNav: GlobalNavProvider, diff --git a/test/functional/services/query_bar.ts b/test/functional/services/query_bar.ts index 5718835c0b5e4..2c4cd3b8db131 100644 --- a/test/functional/services/query_bar.ts +++ b/test/functional/services/query_bar.ts @@ -45,7 +45,8 @@ export function QueryBarProvider({ getService, getPageObjects }: FtrProviderCont public async clearQuery(): Promise { await this.setQuery(''); - await PageObjects.common.pressTabKey(); + await PageObjects.common.pressTabKey(); // move outside of input into language switcher + await PageObjects.common.pressTabKey(); // move outside of language switcher so time picker appears } public async submitQuery(): Promise { diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index b828012f39307..9cb5e255ec99b 100644 Binary files a/test/interpreter_functional/screenshots/baseline/combined_test.png and b/test/interpreter_functional/screenshots/baseline/combined_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index 4f728f5111748..9cb5e255ec99b 100644 Binary files a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png and b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_all_data.png b/test/interpreter_functional/screenshots/baseline/metric_all_data.png index 0a9475fc710d1..54ee1f4da6684 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_all_data.png and b/test/interpreter_functional/screenshots/baseline/metric_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png index aab2905cee19b..b1448cd7cb2ef 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png and b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png index 2ae380df282ba..3cf9d89c37620 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png and b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png index 03cc2e4d77d37..8a5fd9d7a7285 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png and b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png index 2cf25aff54a73..315653ee2b940 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png and b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_1.png b/test/interpreter_functional/screenshots/baseline/partial_test_1.png index 9d52fb30b7d65..5e43b52099d15 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_1.png and b/test/interpreter_functional/screenshots/baseline/partial_test_1.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_2.png b/test/interpreter_functional/screenshots/baseline/partial_test_2.png index b828012f39307..9cb5e255ec99b 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_2.png and b/test/interpreter_functional/screenshots/baseline/partial_test_2.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_3.png b/test/interpreter_functional/screenshots/baseline/partial_test_3.png index 93a8e53540744..b0edb637e0047 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_3.png and b/test/interpreter_functional/screenshots/baseline/partial_test_3.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png index 4938d13fcb41d..d195403bb26d3 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png index b3703ecc7a330..29a0ace5905dd 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png index c43169bfb7101..b8ffa6e8576fe 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png index f8de00f81926d..f1ea0471c3651 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png index f862a9cd46c66..4b5e445d2d55d 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png differ diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts index 7f6e9a6439165..f71fa58cd7cc5 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts @@ -49,7 +49,7 @@ export default function ({ to: '2015-09-22T00:00:00Z', }; const expression = ` - kibana_context timeRange='${JSON.stringify(timeRange)}' + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} | esaggs index={indexPatternLoad id='logstash-*'} aggs={aggCount id="1" enabled=true schema="metric"} `; @@ -63,7 +63,7 @@ export default function ({ to: '2015-09-22T00:00:00Z', }; const expression = ` - kibana_context timeRange='${JSON.stringify(timeRange)}' + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} | esaggs index={indexPatternLoad id='logstash-*'} timeFields='relatedContent.article:published_time' aggs={aggCount id="1" enabled=true schema="metric"} @@ -78,7 +78,7 @@ export default function ({ to: '2015-09-22T00:00:00Z', }; const expression = ` - kibana_context timeRange='${JSON.stringify(timeRange)}' + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} | esaggs index={indexPatternLoad id='logstash-*'} timeFields='relatedContent.article:published_time' timeFields='@timestamp' diff --git a/tsconfig.base.json b/tsconfig.base.json index 5220601e794b0..865806cffe5bb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -36,8 +36,6 @@ // Resolve modules in the same way as Node.js. Aka make `require` works the // same in TypeScript as it does in Node.js. "moduleResolution": "node", - // Do not resolve the real path of symlinks - "preserveSymlinks": true, // "resolveJsonModule" allows for importing, extracting types from and generating .json files. "resolveJsonModule": true, // Disallow inconsistently-cased references to the same file. diff --git a/x-pack/.telemetryrc.json b/x-pack/.telemetryrc.json index ae85efcda32d5..b0a8b45c02de9 100644 --- a/x-pack/.telemetryrc.json +++ b/x-pack/.telemetryrc.json @@ -1,5 +1,14 @@ -{ - "output": "plugins/telemetry_collection_xpack/schema/xpack_plugins.json", - "root": "plugins/", - "exclude": [] -} +[ + { + "output": "plugins/telemetry_collection_xpack/schema/xpack_plugins.json", + "root": "plugins/", + "exclude": [ + "plugins/monitoring/server/telemetry_collection/" + ] + }, + { + "output": "plugins/telemetry_collection_xpack/schema/xpack_monitoring.json", + "root": "plugins/monitoring/server/telemetry_collection/", + "exclude": [] + } +] diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 64663f8330b24..676ce1d27d2fc 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -571,6 +571,132 @@ describe('7.11.2', () => { } as SavedObjectUnsanitizedDoc; expect(isAnyActionSupportIncidents(doc)).toBe(false); }); + + test('it does not transforms alerts when the right structure connectors is already applied', () => { + const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.server-log', + group: 'threshold met', + params: { + level: 'info', + message: 'log message', + }, + id: '99257478-e591-4560-b264-441bdd4fe1d9', + }, + { + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: 'SN short desc', + description: 'SN desc', + severity: '2', + impact: '2', + urgency: '2', + }, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }); + + expect(migration7112(alert, migrationContext)).toEqual(alert); + }); + + test('if incident attribute is an empty object, copy back the related attributes from subActionParams back to incident', () => { + const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.server-log', + group: 'threshold met', + params: { + level: 'info', + message: 'log message', + }, + id: '99257478-e591-4560-b264-441bdd4fe1d9', + }, + { + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + short_description: 'SN short desc', + description: 'SN desc', + severity: '2', + impact: '2', + urgency: '2', + incident: {}, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }); + + expect(migration7112(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + actions: [ + alert.attributes.actions![0], + { + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: 'SN short desc', + description: 'SN desc', + severity: '2', + impact: '2', + urgency: '2', + }, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }, + }); + }); + + test('custom action does not get migrated/loss', () => { + const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.mike', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + short_description: 'SN short desc', + description: 'SN desc', + severity: '2', + impact: '2', + urgency: '2', + incident: {}, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }); + + expect(migration7112(alert, migrationContext)).toEqual(alert); + }); }); function getUpdatedAt(): string { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index f2f956a0a2b26..729290498561f 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -10,6 +10,7 @@ import { SavedObjectUnsanitizedDoc, SavedObjectMigrationFn, SavedObjectMigrationContext, + SavedObjectAttributes, } from '../../../../../src/core/server'; import { RawAlert, RawAlertAction } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; @@ -180,113 +181,147 @@ function initializeExecutionStatus( }; } +function isEmptyObject(obj: {}) { + for (const attr in obj) { + if (Object.prototype.hasOwnProperty.call(obj, attr)) { + return false; + } + } + return true; +} + function restructureConnectorsThatSupportIncident( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc { const { actions } = doc.attributes; const newActions = actions.reduce((acc, action) => { - if (action.params.subAction !== 'pushToService') { - return [...acc, action]; - } - - if (action.actionTypeId === '.servicenow') { - const { title, comments, comment, description, severity, urgency, impact } = action.params - .subActionParams as { - title: string; - description?: string; - severity?: string; - urgency?: string; - impact?: string; - comment?: string; - comments?: Array<{ commentId: string; comment: string }>; - }; - return [ - ...acc, - { - ...action, - params: { - subAction: 'pushToService', - subActionParams: { - incident: { - short_description: title, - description, - severity, - urgency, - impact, + if ( + ['.servicenow', '.jira', '.resilient'].includes(action.actionTypeId) && + action.params.subAction === 'pushToService' + ) { + // Future developer, we needed to do that because when we created this migration + // we forget to think about user already using 7.11.0 and having an incident attribute build the right way + // IMPORTANT -> if you change this code please do the same inside of this file + // x-pack/plugins/alerting/server/saved_objects/migrations.ts + const subActionParamsIncident = + (action.params?.subActionParams as SavedObjectAttributes)?.incident ?? null; + if (subActionParamsIncident != null && !isEmptyObject(subActionParamsIncident)) { + return [...acc, action]; + } + if (action.actionTypeId === '.servicenow') { + const { + title, + comments, + comment, + description, + severity, + urgency, + impact, + short_description: shortDescription, + } = action.params.subActionParams as { + title: string; + description?: string; + severity?: string; + urgency?: string; + impact?: string; + comment?: string; + comments?: Array<{ commentId: string; comment: string }>; + short_description?: string; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: shortDescription ?? title, + description, + severity, + urgency, + impact, + }, + comments: [ + ...(comments ?? []), + ...(comment != null ? [{ commentId: '1', comment }] : []), + ], }, - comments: [ - ...(comments ?? []), - ...(comment != null ? [{ commentId: '1', comment }] : []), - ], }, }, - }, - ] as RawAlertAction[]; - } - - if (action.actionTypeId === '.jira') { - const { title, comments, description, issueType, priority, labels, parent } = action.params - .subActionParams as { - title: string; - description: string; - issueType: string; - priority?: string; - labels?: string[]; - parent?: string; - comments?: unknown[]; - }; - return [ - ...acc, - { - ...action, - params: { - subAction: 'pushToService', - subActionParams: { - incident: { - summary: title, - description, - issueType, - priority, - labels, - parent, + ] as RawAlertAction[]; + } else if (action.actionTypeId === '.jira') { + const { + title, + comments, + description, + issueType, + priority, + labels, + parent, + summary, + } = action.params.subActionParams as { + title: string; + description: string; + issueType: string; + priority?: string; + labels?: string[]; + parent?: string; + comments?: unknown[]; + summary?: string; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + summary: summary ?? title, + description, + issueType, + priority, + labels, + parent, + }, + comments, }, - comments, }, }, - }, - ] as RawAlertAction[]; - } - - if (action.actionTypeId === '.resilient') { - const { title, comments, description, incidentTypes, severityCode } = action.params - .subActionParams as { - title: string; - description: string; - incidentTypes?: number[]; - severityCode?: number; - comments?: unknown[]; - }; - return [ - ...acc, - { - ...action, - params: { - subAction: 'pushToService', - subActionParams: { - incident: { - name: title, - description, - incidentTypes, - severityCode, + ] as RawAlertAction[]; + } else if (action.actionTypeId === '.resilient') { + const { title, comments, description, incidentTypes, severityCode, name } = action.params + .subActionParams as { + title: string; + description: string; + incidentTypes?: number[]; + severityCode?: number; + comments?: unknown[]; + name?: string; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + name: name ?? title, + description, + incidentTypes, + severityCode, + }, + comments, }, - comments, }, }, - }, - ] as RawAlertAction[]; + ] as RawAlertAction[]; + } } - return acc; + return [...acc, action]; }, [] as RawAlertAction[]); return { diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index e1a322c4b1c94..b8c80a101f4c4 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -171,6 +171,34 @@ describe('case connector', () => { }, }, }, + { + test: 'servicenow-sir', + params: { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'servicenow-sir', + name: 'Servicenow SIR', + type: '.servicenow-sir', + fields: { + destIp: true, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + category: 'ddos', + subcategory: '15', + priority: '1', + }, + }, + settings: { + syncAlerts: true, + }, + }, + }, + }, { test: 'none', params: { @@ -474,7 +502,7 @@ describe('case connector', () => { }); }); - it('succeeds when servicenow fields are valid', () => { + it('succeeds when servicenow ITMSM fields are valid', () => { const params: Record = { subAction: 'update', subActionParams: { @@ -508,6 +536,42 @@ describe('case connector', () => { }); }); + it('succeeds when servicenow SIR fields are valid', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'servicenow-sir', + name: 'Servicenow SIR', + type: '.servicenow-sir', + fields: { + destIp: true, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + category: 'ddos', + subcategory: '15', + priority: '1', + }, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual({ + ...params, + subActionParams: { + description: null, + tags: null, + title: null, + status: null, + settings: null, + ...(params.subActionParams as Record), + }, + }); + }); + it('set fields to null if they are missing', () => { const params: Record = { subAction: 'update', diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index dce18119d1704..803b01cbbdc57 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -56,7 +56,7 @@ const ResilientFieldsSchema = schema.object({ severityCode: schema.nullable(schema.string()), }); -const ServiceNowFieldsSchema = schema.object({ +const ServiceNowITSMFieldsSchema = schema.object({ impact: schema.nullable(schema.string()), severity: schema.nullable(schema.string()), urgency: schema.nullable(schema.string()), @@ -64,11 +64,22 @@ const ServiceNowFieldsSchema = schema.object({ subcategory: schema.nullable(schema.string()), }); +const ServiceNowSIRFieldsSchema = schema.object({ + destIp: schema.nullable(schema.boolean()), + sourceIp: schema.nullable(schema.boolean()), + malwareHash: schema.nullable(schema.boolean()), + malwareUrl: schema.nullable(schema.boolean()), + priority: schema.nullable(schema.string()), + category: schema.nullable(schema.string()), + subcategory: schema.nullable(schema.string()), +}); + const NoneFieldsSchema = schema.nullable(schema.object({})); const ReducedConnectorFieldsSchema: { [x: string]: any } = { '.jira': JiraFieldsSchema, '.resilient': ResilientFieldsSchema, + '.servicenow-sir': ServiceNowSIRFieldsSchema, }; export const ConnectorProps = { @@ -78,6 +89,7 @@ export const ConnectorProps = { schema.literal('.servicenow'), schema.literal('.jira'), schema.literal('.resilient'), + schema.literal('.servicenow-sir'), schema.literal('.none'), ]), // Chain of conditional schemes @@ -92,7 +104,7 @@ export const ConnectorProps = { schema.conditional( schema.siblingRef('type'), '.servicenow', - ServiceNowFieldsSchema, + ServiceNowITSMFieldsSchema, NoneFieldsSchema ) ), diff --git a/x-pack/plugins/data_enhanced/common/search/session/index.ts b/x-pack/plugins/data_enhanced/common/search/session/index.ts index 45b5c16bca957..bc09a1e0351e3 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/index.ts @@ -5,7 +5,12 @@ * 2.0. */ -export * from './status'; -export * from './types'; - -export const SEARCH_SESSIONS_TABLE_ID = 'searchSessionsMgmtUiTable'; +// TODO https://github.com/elastic/kibana/issues/92802 +export { + SEARCH_SESSION_TYPE, + SearchSessionSavedObjectAttributes, + SearchSessionFindOptions, + SearchSessionRequestInfo, + SearchSessionStatus, + SEARCH_SESSIONS_TABLE_ID, +} from '../../../../../../src/plugins/data/common/'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx index 1a2b2cfb4ecec..0f449087c5932 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx @@ -14,6 +14,7 @@ import { DeleteButton } from './delete_button'; import { ExtendButton } from './extend_button'; import { InspectButton } from './inspect_button'; import { ACTION, OnActionComplete } from './types'; +import { RenameButton } from './rename_button'; export const getAction = ( api: SearchSessionsMgmtAPI, @@ -53,6 +54,13 @@ export const getAction = ( ), }; + case ACTION.RENAME: + return { + iconType: 'pencil', + textColor: 'default', + label: , + }; + default: // eslint-disable-next-line no-console console.error(`Unknown action: ${actionType}`); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/rename_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/rename_button.tsx new file mode 100644 index 0000000000000..870de78ea06ca --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/rename_button.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState } from 'react'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { TableText } from '../'; +import { OnActionComplete } from './types'; + +interface RenameButtonProps { + id: string; + name: string; + api: SearchSessionsMgmtAPI; + onActionComplete: OnActionComplete; +} + +const RenameDialog = ({ onDismiss, ...props }: RenameButtonProps & { onDismiss: () => void }) => { + const { id, name: originalName, api, onActionComplete } = props; + const [isLoading, setIsLoading] = useState(false); + const [newName, setNewName] = useState(originalName); + + const title = i18n.translate('xpack.data.mgmt.searchSessions.renameModal.title', { + defaultMessage: 'Edit search session name', + }); + const confirm = i18n.translate('xpack.data.mgmt.searchSessions.renameModal.renameButton', { + defaultMessage: 'Save', + }); + const cancel = i18n.translate('xpack.data.mgmt.searchSessions.renameModal.cancelButton', { + defaultMessage: 'Cancel', + }); + + const label = i18n.translate( + 'xpack.data.mgmt.searchSessions.renameModal.searchSessionNameInputLabel', + { + defaultMessage: 'Search session name', + } + ); + + const isNewNameValid = newName && originalName !== newName; + + return ( + + + {title} + + + + + + setNewName(e.target.value)} + /> + + + + + + {cancel} + + { + if (!isNewNameValid) return; + setIsLoading(true); + await api.sendRename(id, newName); + setIsLoading(false); + onDismiss(); + onActionComplete(); + }} + fill + isLoading={isLoading} + > + {confirm} + + + + ); +}; + +export const RenameButton = (props: RenameButtonProps) => { + const [showRenameDialog, setShowRenameDialog] = useState(false); + + const onClick = () => { + setShowRenameDialog(true); + }; + + const onDismiss = () => { + setShowRenameDialog(false); + }; + + return ( + <> + + + + {showRenameDialog ? : null} + + ); +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts index c94b6aa8495c7..407666adbfab4 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts @@ -11,4 +11,5 @@ export enum ACTION { INSPECT = 'inspect', EXTEND = 'extend', DELETE = 'delete', + RENAME = 'rename', } diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts index 10b2ac3ec1d4c..f1b65e4f338bc 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts @@ -68,6 +68,7 @@ describe('Search Sessions Management API', () => { Object { "actions": Array [ "inspect", + "rename", "extend", "delete", ], diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts index 838b51994aa71..6b311908d0702 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts @@ -25,6 +25,7 @@ type UrlGeneratorsStart = SharePluginStart['urlGenerators']; function getActions(status: SearchSessionStatus) { const actions: ACTION[] = []; actions.push(ACTION.INSPECT); + actions.push(ACTION.RENAME); if (status === SearchSessionStatus.IN_PROGRESS || status === SearchSessionStatus.COMPLETE) { actions.push(ACTION.EXTEND); actions.push(ACTION.DELETE); @@ -202,4 +203,23 @@ export class SearchSessionsMgmtAPI { }); } } + + // Change the user-facing name of a search session + public async sendRename(id: string, newName: string): Promise { + try { + await this.sessionsClient.rename(id, newName); + + this.deps.notifications.toasts.addSuccess({ + title: i18n.translate('xpack.data.mgmt.searchSessions.api.rename', { + defaultMessage: 'The search session was renamed', + }), + }); + } catch (err) { + this.deps.notifications.toasts.addError(err, { + title: i18n.translate('xpack.data.mgmt.searchSessions.api.renameError', { + defaultMessage: 'Failed to rename the search session', + }), + }); + } + } } diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx index 7e2c9c063daa4..43d4367f85940 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx @@ -173,6 +173,11 @@ export const createConnectedSearchSessionIndicator = ({ } }, [state]); + const searchSessionName = useObservable(sessionService.searchSessionName$); + const saveSearchSessionNameFn = useCallback(async (newName: string) => { + await sessionService.renameCurrentSession(newName); + }, []); + if (!sessionService.isSessionStorageReady()) return null; return ( @@ -189,6 +194,8 @@ export const createConnectedSearchSessionIndicator = ({ onOpened={onOpened} onViewSearchSessions={onViewSearchSessions} viewSearchSessionsLink={searchSessionsManagementUrl} + searchSessionName={searchSessionName} + saveSearchSessionNameFn={saveSearchSessionNameFn} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.scss b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/components/index.ts similarity index 84% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.scss rename to x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/components/index.ts index 4d7b53e1b000e..9093e1a2535e2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.scss +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/components/index.ts @@ -5,6 +5,4 @@ * 2.0. */ -.content-section { - padding-bottom: 44px; -} +export * from './search_session_name'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.scss b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/components/search_session_name/index.ts similarity index 51% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.scss rename to x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/components/search_session_name/index.ts index 551d95c3f24b4..9093e1a2535e2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.scss +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/components/search_session_name/index.ts @@ -5,18 +5,4 @@ * 2.0. */ -.wrapped-icon { - width: 30px; - height: 30px; - overflow: hidden; - margin-right: 4px; - position: relative; - display: flex; - justify-content: center; - align-items: center; - - img { - max-width: 100%; - max-height: 100%; - } -} +export * from './search_session_name'; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/components/search_session_name/search_session_name.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/components/search_session_name/search_session_name.tsx new file mode 100644 index 0000000000000..a5088ab19ba38 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/components/search_session_name/search_session_name.tsx @@ -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 React, { useEffect } from 'react'; +import { EuiButtonEmpty, EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +export interface SearchSessionNameProps { + name: string; + editName: (newName: string) => Promise; +} + +export const SearchSessionName: React.FC = ({ name, editName }) => { + const [isEditing, setIsEditing] = React.useState(false); + const [newName, setNewName] = React.useState(name); + + const [isSaving, setIsSaving] = React.useState(false); + const isNewNameValid = !!newName; + + useEffect(() => { + if (!isEditing) { + setNewName(name); + } + }, [isEditing, name]); + + return !isEditing ? ( + + +

{name}

+
+ setIsEditing(true)} + /> +
+ ) : ( + { + setNewName(e.target.value); + }} + aria-label={i18n.translate('xpack.data.searchSessionName.ariaLabelText', { + defaultMessage: 'Search session name', + })} + data-test-subj={'searchSessionNameInput'} + append={ + { + if (!isNewNameValid) return; + if (newName !== name && editName) { + setIsSaving(true); + try { + await editName(newName!); + } catch (e) { + // handled by the service + } + } + + setIsSaving(false); + setIsEditing(false); + }} + disabled={!isNewNameValid} + isLoading={isSaving} + data-test-subj={'searchSessionNameSave'} + > + + + } + /> + ); +}; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx index 62d95c1043800..01b6c89a0ddc7 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx @@ -10,34 +10,58 @@ import { storiesOf } from '@storybook/react'; import { SearchSessionIndicator } from './search_session_indicator'; import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; -storiesOf('components/SearchSessionIndicator', module).add('default', () => ( - <> -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- -)); +storiesOf('components/SearchSessionIndicator', module).add('default', () => { + const [searchSessionName, setSearchSessionName] = React.useState('Discover session'); + + const saveSearchSessionNameFn = (newName: string) => + new Promise((resolve) => { + setTimeout(() => { + setSearchSessionName(newName); + resolve(void 0); + }, 1000); + }); + + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + ); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx index c27a42d8d3d60..d20abd4544a47 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx @@ -21,9 +21,10 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { PartialClock, CheckInEmptyCircle } from './custom_icons'; +import { CheckInEmptyCircle, PartialClock } from './custom_icons'; import './search_session_indicator.scss'; import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; +import { SearchSessionName } from './components'; export interface SearchSessionIndicatorProps { state: SearchSessionState; @@ -38,6 +39,9 @@ export interface SearchSessionIndicatorProps { saveDisabledReasonText?: string; onOpened?: (openedState: SearchSessionState) => void; + + searchSessionName?: string; + saveSearchSessionNameFn?: (newName: string) => Promise; } type ActionButtonProps = SearchSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps }; @@ -365,7 +369,16 @@ export const SearchSessionIndicator = React.forwardRef< } >
- + {props.searchSessionName && props.saveSearchSessionNameFn ? ( + <> + + + + ) : null} +

{popover.title}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts index 8d70f1c049b1f..57af8cada9890 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts @@ -32,8 +32,43 @@ export const SUCCESS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.deleteSuccessMessage', { defaultMessage: 'Successfully removed curation.' } ); +export const RESTORE_CONFIRMATION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.restoreConfirmation', + { + defaultMessage: + 'Are you sure you want to clear your changes and return to your default results?', + } +); export const RESULT_ACTIONS_DIRECTIONS = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.resultActionsDescription', { defaultMessage: 'Promote results by clicking the star, hide them by clicking the eye.' } ); +export const PROMOTE_DOCUMENT_ACTION = { + title: i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.promoteButtonLabel', { + defaultMessage: 'Promote this result', + }), + iconType: 'starPlusEmpty', + iconColor: 'primary', +}; +export const DEMOTE_DOCUMENT_ACTION = { + title: i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.demoteButtonLabel', { + defaultMessage: 'Demote this result', + }), + iconType: 'starMinusFilled', + iconColor: 'primary', +}; +export const HIDE_DOCUMENT_ACTION = { + title: i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.hideButtonLabel', { + defaultMessage: 'Hide this result', + }), + iconType: 'eyeClosed', + iconColor: 'danger', +}; +export const SHOW_DOCUMENT_ACTION = { + title: i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.showButtonLabel', { + defaultMessage: 'Show this result', + }), + iconType: 'eye', + iconColor: 'primary', +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx index 748b5670e1d1d..bbf1b95e251da 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx @@ -12,7 +12,7 @@ import { setMockActions, setMockValues, rerender } from '../../../../__mocks__'; import React from 'react'; import { useParams } from 'react-router-dom'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { EuiPageHeader } from '@elastic/eui'; @@ -34,6 +34,7 @@ describe('Curation', () => { }; const actions = { loadCuration: jest.fn(), + resetCuration: jest.fn(), }; beforeEach(() => { @@ -75,4 +76,33 @@ describe('Curation', () => { rerender(wrapper); expect(actions.loadCuration).toHaveBeenCalledTimes(2); }); + + describe('restore defaults button', () => { + let restoreDefaultsButton: ShallowWrapper; + let confirmSpy: jest.SpyInstance; + + beforeAll(() => { + const wrapper = shallow(); + const headerActions = wrapper.find(EuiPageHeader).prop('rightSideItems'); + restoreDefaultsButton = shallow(headerActions![0] as React.ReactElement); + + confirmSpy = jest.spyOn(window, 'confirm'); + }); + + afterAll(() => { + confirmSpy.mockRestore(); + }); + + it('resets the curation upon user confirmation', () => { + confirmSpy.mockReturnValueOnce(true); + restoreDefaultsButton.simulate('click'); + expect(actions.resetCuration).toHaveBeenCalled(); + }); + + it('does not reset the curation if the user cancels', () => { + confirmSpy.mockReturnValueOnce(false); + restoreDefaultsButton.simulate('click'); + expect(actions.resetCuration).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx index 221c2419b7448..85e91dabc6108 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx @@ -10,17 +10,18 @@ import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; -import { EuiPageHeader, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiPageHeader, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FlashMessages } from '../../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; import { Loading } from '../../../../shared/loading'; -import { MANAGE_CURATION_TITLE } from '../constants'; +import { MANAGE_CURATION_TITLE, RESTORE_CONFIRMATION } from '../constants'; import { CurationLogic } from './curation_logic'; -import { OrganicDocuments } from './documents'; +import { PromotedDocuments, OrganicDocuments, HiddenDocuments } from './documents'; import { ActiveQuerySelect, ManageQueriesModal } from './queries'; interface Props { @@ -29,7 +30,7 @@ interface Props { export const Curation: React.FC = ({ curationsBreadcrumb }) => { const { curationId } = useParams() as { curationId: string }; - const { loadCuration } = useActions(CurationLogic({ curationId })); + const { loadCuration, resetCuration } = useActions(CurationLogic({ curationId })); const { dataLoading, queries } = useValues(CurationLogic({ curationId })); useEffect(() => { @@ -43,7 +44,18 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => { { + if (window.confirm(RESTORE_CONFIRMATION)) resetCuration(); + }} + > + {i18n.translate('xpack.enterpriseSearch.appSearch.actions.restoreDefaults', { + defaultMessage: 'Restore defaults', + })} + , + ]} responsive={false} /> @@ -59,9 +71,11 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => { - {/* TODO: PromotedDocuments section */} + + - {/* TODO: HiddenDocuments section */} + + {/* TODO: AddResult flyout */} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts index 821dd21478027..17f7cd7cd154e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts @@ -51,6 +51,10 @@ describe('CurationLogic', () => { queriesLoading: false, activeQuery: '', organicDocumentsLoading: false, + promotedIds: [], + promotedDocumentsLoading: false, + hiddenIds: [], + hiddenDocumentsLoading: false, }; beforeEach(() => { @@ -64,7 +68,7 @@ describe('CurationLogic', () => { describe('actions', () => { describe('onCurationLoad', () => { - it('should set curation, queries, activeQuery, & all loading states to false', () => { + it('should set curation, queries, activeQuery, promotedIds, hiddenIds, & all loading states to false', () => { mount(); CurationLogic.actions.onCurationLoad(MOCK_CURATION_RESPONSE); @@ -74,9 +78,13 @@ describe('CurationLogic', () => { curation: MOCK_CURATION_RESPONSE, queries: ['some search'], activeQuery: 'some search', + promotedIds: ['some-promoted-document'], + hiddenIds: ['some-hidden-document'], dataLoading: false, queriesLoading: false, organicDocumentsLoading: false, + promotedDocumentsLoading: false, + hiddenDocumentsLoading: false, }); }); @@ -95,6 +103,8 @@ describe('CurationLogic', () => { dataLoading: true, queriesLoading: true, organicDocumentsLoading: true, + promotedDocumentsLoading: true, + hiddenDocumentsLoading: true, }); CurationLogic.actions.onCurationError(); @@ -104,6 +114,8 @@ describe('CurationLogic', () => { dataLoading: false, queriesLoading: false, organicDocumentsLoading: false, + promotedDocumentsLoading: false, + hiddenDocumentsLoading: false, }); }); }); @@ -136,6 +148,121 @@ describe('CurationLogic', () => { }); }); }); + + describe('setPromotedIds', () => { + it('should set promotedIds state & promotedDocumentsLoading to true', () => { + mount(); + + CurationLogic.actions.setPromotedIds(['hello', 'world']); + + expect(CurationLogic.values).toEqual({ + ...DEFAULT_VALUES, + promotedIds: ['hello', 'world'], + promotedDocumentsLoading: true, + }); + }); + }); + + describe('addPromotedId', () => { + it('should set promotedIds state & promotedDocumentsLoading to true', () => { + mount({ promotedIds: ['hello'] }); + + CurationLogic.actions.addPromotedId('world'); + + expect(CurationLogic.values).toEqual({ + ...DEFAULT_VALUES, + promotedIds: ['hello', 'world'], + promotedDocumentsLoading: true, + }); + }); + }); + + describe('removePromotedId', () => { + it('should set promotedIds state & promotedDocumentsLoading to true', () => { + mount({ promotedIds: ['hello', 'deleteme', 'world'] }); + + CurationLogic.actions.removePromotedId('deleteme'); + + expect(CurationLogic.values).toEqual({ + ...DEFAULT_VALUES, + promotedIds: ['hello', 'world'], + promotedDocumentsLoading: true, + }); + }); + }); + + describe('clearPromotedId', () => { + it('should reset promotedIds state & set promotedDocumentsLoading to true', () => { + mount({ promotedIds: ['hello', 'world'] }); + + CurationLogic.actions.clearPromotedIds(); + + expect(CurationLogic.values).toEqual({ + ...DEFAULT_VALUES, + promotedIds: [], + promotedDocumentsLoading: true, + }); + }); + }); + + describe('addHiddenId', () => { + it('should set hiddenIds state & hiddenDocumentsLoading to true', () => { + mount({ hiddenIds: ['hello'] }); + + CurationLogic.actions.addHiddenId('world'); + + expect(CurationLogic.values).toEqual({ + ...DEFAULT_VALUES, + hiddenIds: ['hello', 'world'], + hiddenDocumentsLoading: true, + }); + }); + }); + + describe('removeHiddenId', () => { + it('should set hiddenIds state & hiddenDocumentsLoading to true', () => { + mount({ hiddenIds: ['hello', 'deleteme', 'world'] }); + + CurationLogic.actions.removeHiddenId('deleteme'); + + expect(CurationLogic.values).toEqual({ + ...DEFAULT_VALUES, + hiddenIds: ['hello', 'world'], + hiddenDocumentsLoading: true, + }); + }); + }); + + describe('clearHiddenId', () => { + it('should reset hiddenIds state & set hiddenDocumentsLoading to true', () => { + mount({ hiddenIds: ['hello', 'world'] }); + + CurationLogic.actions.clearHiddenIds(); + + expect(CurationLogic.values).toEqual({ + ...DEFAULT_VALUES, + hiddenIds: [], + hiddenDocumentsLoading: true, + }); + }); + }); + + describe('resetCuration', () => { + it('should clear promotedIds & hiddenIds & set dataLoading to true', () => { + mount({ promotedIds: ['hello'], hiddenIds: ['world'] }); + + CurationLogic.actions.resetCuration(); + + expect(CurationLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + promotedIds: [], + promotedDocumentsLoading: true, + hiddenIds: [], + hiddenDocumentsLoading: true, + }); + }); + }); }); describe('listeners', () => { @@ -187,6 +314,8 @@ describe('CurationLogic', () => { { queries: ['a', 'b', 'c'], activeQuery: 'b', + promotedIds: ['d', 'e', 'f'], + hiddenIds: ['g'], }, { curationId: 'cur-123456789' } ); @@ -199,7 +328,7 @@ describe('CurationLogic', () => { expect(http.put).toHaveBeenCalledWith( '/api/app_search/engines/some-engine/curations/cur-123456789', { - body: '{"queries":["a","b","c"],"query":"b","promoted":[],"hidden":[]}', // Uses state currently in CurationLogic + body: '{"queries":["a","b","c"],"query":"b","promoted":["d","e","f"],"hidden":["g"]}', // Uses state currently in CurationLogic } ); expect(CurationLogic.actions.onCurationLoad).toHaveBeenCalledWith(MOCK_CURATION_RESPONSE); @@ -249,6 +378,34 @@ describe('CurationLogic', () => { it('setActiveQuery', () => { CurationLogic.actions.setActiveQuery('test'); }); + + it('setPromotedIds', () => { + CurationLogic.actions.setPromotedIds(['test']); + }); + + it('addPromotedId', () => { + CurationLogic.actions.addPromotedId('test'); + }); + + it('removePromotedId', () => { + CurationLogic.actions.removePromotedId('test'); + }); + + it('clearPromotedIds', () => { + CurationLogic.actions.clearPromotedIds(); + }); + + it('addHiddenId', () => { + CurationLogic.actions.addHiddenId('test'); + }); + + it('removeHiddenId', () => { + CurationLogic.actions.removeHiddenId('test'); + }); + + it('clearHiddenIds', () => { + CurationLogic.actions.clearHiddenIds(); + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts index c3ee1aac57ace..9fa2d353b4ef2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts @@ -14,6 +14,7 @@ import { ENGINE_CURATIONS_PATH } from '../../../routes'; import { EngineLogic, generateEnginePath } from '../../engine'; import { Curation } from '../types'; +import { addDocument, removeDocument } from '../utils'; interface CurationValues { dataLoading: boolean; @@ -22,6 +23,10 @@ interface CurationValues { queriesLoading: boolean; activeQuery: string; organicDocumentsLoading: boolean; + promotedIds: string[]; + promotedDocumentsLoading: boolean; + hiddenIds: string[]; + hiddenDocumentsLoading: boolean; } interface CurationActions { @@ -31,6 +36,14 @@ interface CurationActions { onCurationError(): void; updateQueries(queries: Curation['queries']): { queries: Curation['queries'] }; setActiveQuery(query: string): { query: string }; + setPromotedIds(promotedIds: string[]): { promotedIds: string[] }; + addPromotedId(id: string): { id: string }; + removePromotedId(id: string): { id: string }; + clearPromotedIds(): void; + addHiddenId(id: string): { id: string }; + removeHiddenId(id: string): { id: string }; + clearHiddenIds(): void; + resetCuration(): void; } interface CurationProps { @@ -46,12 +59,21 @@ export const CurationLogic = kea ({ queries }), setActiveQuery: (query) => ({ query }), + setPromotedIds: (promotedIds) => ({ promotedIds }), + addPromotedId: (id) => ({ id }), + removePromotedId: (id) => ({ id }), + clearPromotedIds: true, + addHiddenId: (id) => ({ id }), + removeHiddenId: (id) => ({ id }), + clearHiddenIds: true, + resetCuration: true, }), reducers: () => ({ dataLoading: [ true, { loadCuration: () => true, + resetCuration: () => true, onCurationLoad: () => false, onCurationError: () => false, }, @@ -99,6 +121,46 @@ export const CurationLogic = kea false, }, ], + promotedIds: [ + [], + { + onCurationLoad: (_, { curation }) => curation.promoted.map((document) => document.id), + setPromotedIds: (_, { promotedIds }) => promotedIds, + addPromotedId: (promotedIds, { id }) => addDocument(promotedIds, id), + removePromotedId: (promotedIds, { id }) => removeDocument(promotedIds, id), + clearPromotedIds: () => [], + }, + ], + promotedDocumentsLoading: [ + false, + { + setPromotedIds: () => true, + addPromotedId: () => true, + removePromotedId: () => true, + clearPromotedIds: () => true, + onCurationLoad: () => false, + onCurationError: () => false, + }, + ], + hiddenIds: [ + [], + { + onCurationLoad: (_, { curation }) => curation.hidden.map((document) => document.id), + addHiddenId: (hiddenIds, { id }) => addDocument(hiddenIds, id), + removeHiddenId: (hiddenIds, { id }) => removeDocument(hiddenIds, id), + clearHiddenIds: () => [], + }, + ], + hiddenDocumentsLoading: [ + false, + { + addHiddenId: () => true, + removeHiddenId: () => true, + clearHiddenIds: () => true, + onCurationLoad: () => false, + onCurationError: () => false, + }, + ], }), listeners: ({ actions, values, props }) => ({ loadCuration: async () => { @@ -131,8 +193,8 @@ export const CurationLogic = kea actions.updateCuration(), + setPromotedIds: () => actions.updateCuration(), + addPromotedId: () => actions.updateCuration(), + removePromotedId: () => actions.updateCuration(), + clearPromotedIds: () => actions.updateCuration(), + addHiddenId: () => actions.updateCuration(), + removeHiddenId: () => actions.updateCuration(), + clearHiddenIds: () => actions.updateCuration(), + resetCuration: () => { + actions.clearPromotedIds(); + actions.clearHiddenIds(); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.test.tsx new file mode 100644 index 0000000000000..7ffa45c285320 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButtonEmpty } from '@elastic/eui'; + +import { DataPanel } from '../../../data_panel'; +import { CurationResult } from '../results'; + +import { HiddenDocuments } from './'; + +describe('HiddenDocuments', () => { + const values = { + curation: { + hidden: [ + { id: 'mock-document-1' }, + { id: 'mock-document-2' }, + { id: 'mock-document-3' }, + { id: 'mock-document-4' }, + { id: 'mock-document-5' }, + ], + }, + hiddenDocumentsLoading: false, + }; + const actions = { + removeHiddenId: jest.fn(), + clearHiddenIds: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders a list of hidden documents', () => { + const wrapper = shallow(); + + expect(wrapper.find(CurationResult)).toHaveLength(5); + }); + + it('renders an empty state & hides the panel actions when empty', () => { + setMockValues({ ...values, curation: { hidden: [] } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + expect(wrapper.find(DataPanel).prop('action')).toBe(false); + }); + + it('renders a loading state', () => { + setMockValues({ ...values, hiddenDocumentsLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(DataPanel).prop('isLoading')).toEqual(true); + }); + + describe('actions', () => { + it('renders results with an action button that un-hides the result', () => { + const wrapper = shallow(); + const result = wrapper.find(CurationResult).last(); + result.prop('actions')[0].onClick(); + + expect(actions.removeHiddenId).toHaveBeenCalledWith('mock-document-5'); + }); + + it('renders a restore all button that un-hides all hidden results', () => { + const wrapper = shallow(); + const panelActions = shallow(wrapper.find(DataPanel).prop('action') as React.ReactElement); + + panelActions.find(EuiButtonEmpty).simulate('click'); + expect(actions.clearHiddenIds).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx new file mode 100644 index 0000000000000..f2bc416b00341 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx @@ -0,0 +1,99 @@ +/* + * 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 { useValues, useActions } from 'kea'; + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DataPanel } from '../../../data_panel'; + +import { SHOW_DOCUMENT_ACTION } from '../../constants'; +import { CurationLogic } from '../curation_logic'; +import { AddResultButton, CurationResult, convertToResultFormat } from '../results'; + +export const HiddenDocuments: React.FC = () => { + const { clearHiddenIds, removeHiddenId } = useActions(CurationLogic); + const { curation, hiddenDocumentsLoading } = useValues(CurationLogic); + + const documents = curation.hidden; + const hasDocuments = documents.length > 0; + + return ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.title', + { defaultMessage: 'Hidden documents' } + )} + + } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.description', + { defaultMessage: 'Hidden documents will not appear in organic results.' } + )} + action={ + hasDocuments && ( + + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.removeAllButtonLabel', + { defaultMessage: 'Restore all' } + )} + + + + ) + } + isLoading={hiddenDocumentsLoading} + > + {hasDocuments ? ( + documents.map((document) => ( + removeHiddenId(document.id), + }, + ]} + /> + )) + ) : ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.emptyTitle', + { defaultMessage: 'No documents are being hidden for this query' } + )} + + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.emptyDescription', + { + defaultMessage: + 'Hide documents by clicking the eye icon on the organic results above, or search and hide a result manually.', + } + )} + actions={} + /> + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/index.ts index fdaadeb5ced95..3548f6f298069 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/index.ts @@ -5,4 +5,6 @@ * 2.0. */ +export { PromotedDocuments } from './promoted_documents'; export { OrganicDocuments } from './organic_documents'; +export { HiddenDocuments } from './hidden_documents'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx index fd26cb1acf7a6..2a83ecfcada44 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { setMockValues } from '../../../../../__mocks__'; +import { setMockValues, setMockActions } from '../../../../../__mocks__'; import React from 'react'; @@ -31,10 +31,15 @@ describe('OrganicDocuments', () => { activeQuery: 'world', organicDocumentsLoading: false, }; + const actions = { + addPromotedId: jest.fn(), + addHiddenId: jest.fn(), + }; beforeEach(() => { jest.clearAllMocks(); setMockValues(values); + setMockActions(actions); }); it('renders a list of organic results', () => { @@ -64,4 +69,22 @@ describe('OrganicDocuments', () => { expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); }); + + describe('actions', () => { + it('renders results with an action button that promotes the result', () => { + const wrapper = shallow(); + const result = wrapper.find(CurationResult).first(); + result.prop('actions')[1].onClick(); + + expect(actions.addPromotedId).toHaveBeenCalledWith('mock-document-1'); + }); + + it('renders results with an action button that hides the result', () => { + const wrapper = shallow(); + const result = wrapper.find(CurationResult).last(); + result.prop('actions')[0].onClick(); + + expect(actions.addHiddenId).toHaveBeenCalledWith('mock-document-3'); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx index 3aa65a14e7a2f..a3a761feefcd2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { useValues } from 'kea'; +import { useValues, useActions } from 'kea'; import { EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -15,11 +15,16 @@ import { i18n } from '@kbn/i18n'; import { DataPanel } from '../../../data_panel'; import { Result } from '../../../result/types'; -import { RESULT_ACTIONS_DIRECTIONS } from '../../constants'; +import { + RESULT_ACTIONS_DIRECTIONS, + PROMOTE_DOCUMENT_ACTION, + HIDE_DOCUMENT_ACTION, +} from '../../constants'; import { CurationLogic } from '../curation_logic'; import { CurationResult } from '../results'; export const OrganicDocuments: React.FC = () => { + const { addPromotedId, addHiddenId } = useActions(CurationLogic); const { curation, activeQuery, organicDocumentsLoading } = useValues(CurationLogic); const documents = curation.organic; @@ -48,7 +53,16 @@ export const OrganicDocuments: React.FC = () => { addHiddenId(document.id.raw), + }, + { + ...PROMOTE_DOCUMENT_ACTION, + onClick: () => addPromotedId(document.id.raw), + }, + ]} /> )) ) : organicDocumentsLoading ? ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx new file mode 100644 index 0000000000000..7240a443b76e9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiDragDropContext, EuiDraggable, EuiEmptyPrompt, EuiButtonEmpty } from '@elastic/eui'; + +import { DataPanel } from '../../../data_panel'; +import { CurationResult } from '../results'; + +import { PromotedDocuments } from './'; + +describe('PromotedDocuments', () => { + const values = { + curation: { + promoted: [ + { id: 'mock-document-1' }, + { id: 'mock-document-2' }, + { id: 'mock-document-3' }, + { id: 'mock-document-4' }, + ], + }, + promotedIds: ['mock-document-1', 'mock-document-2', 'mock-document-3', 'mock-document-4'], + promotedDocumentsLoading: false, + }; + const actions = { + setPromotedIds: jest.fn(), + clearPromotedIds: jest.fn(), + removePromotedId: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + const getDraggableChildren = (draggableWrapper: any) => { + return draggableWrapper.renderProp('children')({}, {}, {}); + }; + + it('renders a list of draggable promoted documents', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiDraggable)).toHaveLength(4); + + wrapper.find(EuiDraggable).forEach((draggableWrapper) => { + expect(getDraggableChildren(draggableWrapper).find(CurationResult).exists()).toBe(true); + }); + }); + + it('renders an empty state & hides the panel actions when empty', () => { + setMockValues({ ...values, curation: { promoted: [] } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + expect(wrapper.find(DataPanel).prop('action')).toBe(false); + }); + + it('renders a loading state', () => { + setMockValues({ ...values, promotedDocumentsLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(DataPanel).prop('isLoading')).toEqual(true); + }); + + describe('actions', () => { + it('renders results with an action button that demotes the result', () => { + const wrapper = shallow(); + const result = getDraggableChildren(wrapper.find(EuiDraggable).last()); + result.prop('actions')[0].onClick(); + + expect(actions.removePromotedId).toHaveBeenCalledWith('mock-document-4'); + }); + + it('renders a demote all button that demotes all hidden results', () => { + const wrapper = shallow(); + const panelActions = shallow(wrapper.find(DataPanel).prop('action') as React.ReactElement); + + panelActions.find(EuiButtonEmpty).simulate('click'); + expect(actions.clearPromotedIds).toHaveBeenCalled(); + }); + + describe('draggging', () => { + it('calls setPromotedIds with the reordered list when users are done dragging', () => { + const wrapper = shallow(); + wrapper.find(EuiDragDropContext).simulate('dragEnd', { + source: { index: 3 }, + destination: { index: 0 }, + }); + + expect(actions.setPromotedIds).toHaveBeenCalledWith([ + 'mock-document-4', + 'mock-document-1', + 'mock-document-2', + 'mock-document-3', + ]); + }); + + it('does not error if source/destination are unavailable on drag end', () => { + const wrapper = shallow(); + wrapper.find(EuiDragDropContext).simulate('dragEnd', {}); + + expect(actions.setPromotedIds).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx new file mode 100644 index 0000000000000..6b0a02aa2af58 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx @@ -0,0 +1,124 @@ +/* + * 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 { useValues, useActions } from 'kea'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiEmptyPrompt, + EuiButtonEmpty, + EuiDragDropContext, + DropResult, + EuiDroppable, + EuiDraggable, + euiDragDropReorder, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DataPanel } from '../../../data_panel'; + +import { DEMOTE_DOCUMENT_ACTION } from '../../constants'; +import { CurationLogic } from '../curation_logic'; +import { AddResultButton, CurationResult, convertToResultFormat } from '../results'; + +export const PromotedDocuments: React.FC = () => { + const { curation, promotedIds, promotedDocumentsLoading } = useValues(CurationLogic); + const documents = curation.promoted; + const hasDocuments = documents.length > 0; + + const { setPromotedIds, clearPromotedIds, removePromotedId } = useActions(CurationLogic); + const reorderPromotedIds = ({ source, destination }: DropResult) => { + if (source && destination) { + const reorderedIds = euiDragDropReorder(promotedIds, source.index, destination.index); + setPromotedIds(reorderedIds); + } + }; + + return ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.title', + { defaultMessage: 'Promoted documents' } + )} + + } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.description', + { + defaultMessage: + 'Promoted results appear before organic results. Documents can be re-ordered.', + } + )} + action={ + hasDocuments && ( + + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.removeAllButtonLabel', + { defaultMessage: 'Demote all' } + )} + + + + ) + } + isLoading={promotedDocumentsLoading} + > + {hasDocuments ? ( + + + {documents.map((document, i: number) => ( + + {(provided) => ( + removePromotedId(document.id), + }, + ]} + dragHandleProps={provided.dragHandleProps} + /> + )} + + ))} + + + ) : ( + } + /> + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx new file mode 100644 index 0000000000000..78f5325ee567b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { AddResultButton } from './'; + +describe('AddResultButton', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('renders', () => { + expect(wrapper.find(EuiButton)).toHaveLength(1); + }); + + it('opens the add result flyout on click', () => { + wrapper.find(EuiButton).simulate('click'); + // TODO: assert on logic action + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx new file mode 100644 index 0000000000000..9bbc62ae51a0b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const AddResultButton: React.FC = () => { + return ( + {} /* TODO */} iconType="plusInCircle" size="s" fill> + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.addResult.buttonLabel', { + defaultMessage: 'Add result manually', + })} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/index.ts index bbdb87bbe4fa9..3c6339f0c1942 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/index.ts @@ -5,4 +5,6 @@ * 2.0. */ +export { AddResultButton } from './add_result_button'; export { CurationResult } from './curation_result'; +export { convertToResultFormat } from './utils'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/utils.test.ts new file mode 100644 index 0000000000000..7bc05f34511a0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/utils.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { convertToResultFormat, convertIdToMeta } from './utils'; + +describe('convertToResultFormat', () => { + it('converts curation results to a format that the Result component can use', () => { + expect( + convertToResultFormat({ + id: 'some-id', + someField: 'some flat string', + anotherField: '123456', + }) + ).toEqual({ + _meta: { + id: 'some-id', + }, + id: { + raw: 'some-id', + snippet: null, + }, + someField: { + raw: 'some flat string', + snippet: null, + }, + anotherField: { + raw: '123456', + snippet: null, + }, + }); + }); +}); + +describe('convertIdToMeta', () => { + it('creates an approximate _meta object based on the curation result ID', () => { + expect(convertIdToMeta('some-id')).toEqual({ id: 'some-id' }); + expect(convertIdToMeta('some-engine|some-id')).toEqual({ + id: 'some-id', + engine: 'some-engine', + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/utils.ts new file mode 100644 index 0000000000000..b5a5bf1b5a90a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/utils.ts @@ -0,0 +1,52 @@ +/* + * 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 { Result } from '../../../result/types'; +import { CurationResult } from '../../types'; + +/** + * The `promoted` and `hidden` keys from the internal curations endpoints + * currently return a document data structure that our Result component can't + * correctly parse - we need to attempt to naively transform the data in order + * to display it in a Result + * + * TODO: Ideally someday we can update our internal curations endpoint to return + * the same Result-ready data structure that the `organic` endpoint uses, and + * remove this file when that happens + */ + +export const convertToResultFormat = (document: CurationResult): Result => { + const result = {} as Result; + + // Convert `key: 'value'` into `key: { raw: 'value' }` + Object.entries(document).forEach(([key, value]) => { + result[key] = { + raw: value, + snippet: null, // Don't try to provide a snippet, we can't really guesstimate it + }; + }); + + // Add the _meta obj needed by Result + result._meta = convertIdToMeta(document.id); + + return result; +}; + +export const convertIdToMeta = (id: string): Result['_meta'] => { + const splitId = id.split('|'); + const isMetaEngine = splitId.length > 1; + + return isMetaEngine + ? { + engine: splitId[0], + id: splitId[1], + } + : ({ id } as Result['_meta']); + // Note: We're casting this as _meta even though `engine` is missing, + // since for source engines the engine shouldn't matter / be displayed, + // but if needed we could likely populate this from EngineLogic.values +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts index 435b76458db06..51618ed4e3741 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { convertToDate } from './utils'; +import { convertToDate, addDocument, removeDocument } from './utils'; describe('convertToDate', () => { it('converts the English-only server timestamps to a parseable Date', () => { @@ -16,3 +16,23 @@ describe('convertToDate', () => { expect(date.getFullYear()).toEqual(1970); }); }); + +describe('addDocument', () => { + it('adds a new document to the end of the document array without mutating the original array', () => { + const originalDocuments = ['hello']; + const newDocuments = addDocument(originalDocuments, 'world'); + + expect(newDocuments).toEqual(['hello', 'world']); + expect(newDocuments).not.toBe(originalDocuments); // Would fail if we had mutated the array + }); +}); + +describe('removeDocument', () => { + it('removes a specific document from the array without mutating the original array', () => { + const originalDocuments = ['lorem', 'ipsum', 'dolor', 'sit', 'amet']; + const newDocuments = removeDocument(originalDocuments, 'dolor'); + + expect(newDocuments).toEqual(['lorem', 'ipsum', 'sit', 'amet']); + expect(newDocuments).not.toBe(originalDocuments); // Would fail if we had mutated the array + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts index 2ef73e1de4e91..8af2636128304 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts @@ -14,3 +14,14 @@ export const convertToDate = (serverDateString: string): Date => { .replace('AM', ' AM'); return new Date(readableDateString); }; + +export const addDocument = (documentArray: string[], newDocument: string) => { + return [...documentArray, newDocument]; +}; + +export const removeDocument = (documentArray: string[], deletedDocument: string) => { + const newArray = [...documentArray]; + const indexToDelete = newArray.indexOf(deletedDocument); + newArray.splice(indexToDelete, 1); + return newArray; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/constants.ts index 433f23db75626..d5e7035348b45 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/constants.ts @@ -7,7 +7,21 @@ import { i18n } from '@kbn/i18n'; +import { FieldResultSetting } from './types'; + export const RESULT_SETTINGS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.title', { defaultMessage: 'Result Settings' } ); + +export const DEFAULT_FIELD_SETTINGS: FieldResultSetting = { + raw: true, + snippet: false, + snippetFallback: false, +}; + +export const DISABLED_FIELD_SETTINGS: FieldResultSetting = { + raw: false, + snippet: false, + snippetFallback: false, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/index.ts index b605aa5714e91..3336de732a508 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/index.ts @@ -6,4 +6,5 @@ */ export { RESULT_SETTINGS_TITLE } from './constants'; +export { ResultSettingsLogic } from './result_settings_logic'; export { ResultSettings } from './result_settings'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts new file mode 100644 index 0000000000000..9147940374645 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts @@ -0,0 +1,398 @@ +/* + * 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 { Schema, SchemaConflicts, SchemaTypes } from '../../../shared/types'; + +import { OpenModal, ServerFieldResultSettingObject } from './types'; + +import { ResultSettingsLogic } from '.'; + +describe('ResultSettingsLogic', () => { + const { mount } = new LogicMounter(ResultSettingsLogic); + + const DEFAULT_VALUES = { + dataLoading: true, + saving: false, + openModal: OpenModal.None, + nonTextResultFields: {}, + resultFields: {}, + serverResultFields: {}, + textResultFields: {}, + lastSavedResultFields: {}, + schema: {}, + schemaConflicts: {}, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(ResultSettingsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('initializeResultFields', () => { + const serverResultFields: ServerFieldResultSettingObject = { + foo: { raw: { size: 5 } }, + bar: { raw: { size: 5 } }, + }; + const schema: Schema = { + foo: 'text' as SchemaTypes, + bar: 'number' as SchemaTypes, + baz: 'text' as SchemaTypes, + }; + const schemaConflicts: SchemaConflicts = { + foo: { + text: ['foo'], + number: ['foo'], + geolocation: [], + date: [], + }, + }; + + it('will initialize all result field state within the UI, based on provided server data', () => { + mount({ + dataLoading: true, + saving: true, + openModal: OpenModal.ConfirmSaveModal, + }); + + ResultSettingsLogic.actions.initializeResultFields( + serverResultFields, + schema, + schemaConflicts + ); + + expect(ResultSettingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + saving: false, + // It converts the passed server result fields to a client results field and stores it + // as resultFields + resultFields: { + foo: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + // Baz was not part of the original serverResultFields, it was injected based on the schema + baz: { + raw: false, + snippet: false, + snippetFallback: false, + }, + bar: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + }, + // It also saves it as lastSavedResultFields + lastSavedResultFields: { + foo: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + // Baz was not part of the original serverResultFields, it was injected based on the schema + baz: { + raw: false, + snippet: false, + snippetFallback: false, + }, + bar: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + }, + // The resultFields are also partitioned to either nonTextResultFields or textResultFields + // depending on their type within the passed schema + nonTextResultFields: { + bar: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + }, + textResultFields: { + // Baz was not part of the original serverResultFields, it was injected based on the schema + baz: { + raw: false, + snippet: false, + snippetFallback: false, + }, + foo: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + }, + // It stores the originally passed results as serverResultFields + serverResultFields: { + foo: { raw: { size: 5 } }, + // Baz was not part of the original serverResultFields, it was injected based on the schema + baz: {}, + bar: { raw: { size: 5 } }, + }, + // The modal should be reset back to closed if it had been opened previously + openModal: OpenModal.None, + // Stores the provided schema details + schema, + schemaConflicts, + }); + }); + + it('default schema conflicts data if none was provided', () => { + mount(); + + ResultSettingsLogic.actions.initializeResultFields(serverResultFields, schema); + + expect(ResultSettingsLogic.values.schemaConflicts).toEqual({}); + }); + }); + + describe('openConfirmSaveModal', () => { + mount({ + openModal: OpenModal.None, + }); + + ResultSettingsLogic.actions.openConfirmSaveModal(); + + expect(ResultSettingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + openModal: OpenModal.ConfirmSaveModal, + }); + }); + + describe('openConfirmResetModal', () => { + mount({ + openModal: OpenModal.None, + }); + + ResultSettingsLogic.actions.openConfirmResetModal(); + + expect(ResultSettingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + openModal: OpenModal.ConfirmResetModal, + }); + }); + + describe('closeModals', () => { + it('should close open modals', () => { + mount({ + openModal: OpenModal.ConfirmSaveModal, + }); + + ResultSettingsLogic.actions.closeModals(); + + expect(ResultSettingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + openModal: OpenModal.None, + }); + }); + }); + + describe('clearAllFields', () => { + it('should remove all settings that have been set for each field', () => { + mount({ + nonTextResultFields: { + foo: { raw: false, snippet: false, snippetFallback: false }, + bar: { raw: true, snippet: false, snippetFallback: true }, + }, + textResultFields: { + qux: { raw: false, snippet: false, snippetFallback: false }, + quux: { raw: true, snippet: false, snippetFallback: true }, + }, + resultFields: { + quuz: { raw: false, snippet: false, snippetFallback: false }, + corge: { raw: true, snippet: false, snippetFallback: true }, + }, + serverResultFields: { + grault: { raw: { size: 5 } }, + garply: { raw: true }, + }, + }); + + ResultSettingsLogic.actions.clearAllFields(); + + expect(ResultSettingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + nonTextResultFields: { + foo: {}, + bar: {}, + }, + textResultFields: { + qux: {}, + quux: {}, + }, + resultFields: { + quuz: {}, + corge: {}, + }, + serverResultFields: { + grault: {}, + garply: {}, + }, + }); + }); + }); + + describe('resetAllFields', () => { + it('should reset all settings to their default values per field', () => { + mount({ + nonTextResultFields: { + foo: { raw: true, snippet: true, snippetFallback: true }, + bar: { raw: true, snippet: true, snippetFallback: true }, + }, + textResultFields: { + qux: { raw: true, snippet: true, snippetFallback: true }, + quux: { raw: true, snippet: true, snippetFallback: true }, + }, + resultFields: { + quuz: { raw: true, snippet: true, snippetFallback: true }, + corge: { raw: true, snippet: true, snippetFallback: true }, + }, + serverResultFields: { + grault: { raw: { size: 5 } }, + garply: { raw: true }, + }, + }); + + ResultSettingsLogic.actions.resetAllFields(); + + expect(ResultSettingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + nonTextResultFields: { + bar: { raw: true, snippet: false, snippetFallback: false }, + foo: { raw: true, snippet: false, snippetFallback: false }, + }, + textResultFields: { + qux: { raw: true, snippet: false, snippetFallback: false }, + quux: { raw: true, snippet: false, snippetFallback: false }, + }, + resultFields: { + quuz: { raw: true, snippet: false, snippetFallback: false }, + corge: { raw: true, snippet: false, snippetFallback: false }, + }, + serverResultFields: { + grault: { raw: {} }, + garply: { raw: {} }, + }, + }); + }); + + it('should close open modals', () => { + mount({ + openModal: OpenModal.ConfirmSaveModal, + }); + + ResultSettingsLogic.actions.resetAllFields(); + + expect(ResultSettingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + openModal: OpenModal.None, + }); + }); + }); + + describe('updateField', () => { + const initialValues = { + nonTextResultFields: { + foo: { raw: true, snippet: true, snippetFallback: true }, + bar: { raw: true, snippet: true, snippetFallback: true }, + }, + textResultFields: { + foo: { raw: true, snippet: true, snippetFallback: true }, + bar: { raw: true, snippet: true, snippetFallback: true }, + }, + resultFields: { + foo: { raw: true, snippet: true, snippetFallback: true }, + bar: { raw: true, snippet: true, snippetFallback: true }, + }, + serverResultFields: { + foo: { raw: { size: 5 } }, + bar: { raw: true }, + }, + }; + + it('should update settings for an individual field', () => { + mount(initialValues); + + ResultSettingsLogic.actions.updateField('foo', { + raw: true, + snippet: false, + snippetFallback: false, + }); + + expect(ResultSettingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + // the settings for foo are updated below for any *ResultFields state in which they appear + nonTextResultFields: { + foo: { raw: true, snippet: false, snippetFallback: false }, + bar: { raw: true, snippet: true, snippetFallback: true }, + }, + textResultFields: { + foo: { raw: true, snippet: false, snippetFallback: false }, + bar: { raw: true, snippet: true, snippetFallback: true }, + }, + resultFields: { + foo: { raw: true, snippet: false, snippetFallback: false }, + bar: { raw: true, snippet: true, snippetFallback: true }, + }, + serverResultFields: { + // Note that the specified settings for foo get converted to a "server" format here + foo: { raw: {} }, + bar: { raw: true }, + }, + }); + }); + + it('should do nothing if the specified field does not exist', () => { + mount(initialValues); + + ResultSettingsLogic.actions.updateField('baz', { + raw: false, + snippet: false, + snippetFallback: false, + }); + + // 'baz' does not exist in state, so nothing is updated + expect(ResultSettingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + ...initialValues, + }); + }); + }); + + describe('saving', () => { + it('sets saving to true and close any open modals', () => { + mount({ + saving: false, + }); + + ResultSettingsLogic.actions.saving(); + + expect(ResultSettingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + saving: true, + openModal: OpenModal.None, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts new file mode 100644 index 0000000000000..b2ffd3de19f04 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts @@ -0,0 +1,190 @@ +/* + * 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 { Schema, SchemaConflicts } from '../../../shared/types'; + +import { + FieldResultSetting, + FieldResultSettingObject, + OpenModal, + ServerFieldResultSettingObject, +} from './types'; + +import { + clearAllFields, + clearAllServerFields, + convertServerResultFieldsToResultFields, + convertToServerFieldResultSetting, + resetAllFields, + resetAllServerFields, + splitResultFields, +} from './utils'; + +interface ResultSettingsActions { + openConfirmResetModal(): void; + openConfirmSaveModal(): void; + closeModals(): void; + initializeResultFields( + serverResultFields: ServerFieldResultSettingObject, + schema: Schema, + schemaConflicts?: SchemaConflicts + ): { + serverResultFields: ServerFieldResultSettingObject; + resultFields: FieldResultSettingObject; + schema: Schema; + schemaConflicts: SchemaConflicts; + nonTextResultFields: FieldResultSettingObject; + textResultFields: FieldResultSettingObject; + }; + clearAllFields(): void; + resetAllFields(): void; + updateField( + fieldName: string, + settings: FieldResultSetting + ): { fieldName: string; settings: FieldResultSetting }; + saving(): void; +} + +interface ResultSettingsValues { + dataLoading: boolean; + saving: boolean; + openModal: OpenModal; + nonTextResultFields: FieldResultSettingObject; + textResultFields: FieldResultSettingObject; + resultFields: FieldResultSettingObject; + serverResultFields: ServerFieldResultSettingObject; + lastSavedResultFields: FieldResultSettingObject; + schema: Schema; + schemaConflicts: SchemaConflicts; +} + +export const ResultSettingsLogic = kea>({ + path: ['enterprise_search', 'app_search', 'result_settings_logic'], + actions: () => ({ + openConfirmResetModal: () => true, + openConfirmSaveModal: () => true, + closeModals: () => true, + initializeResultFields: (serverResultFields, schema, schemaConflicts) => { + const resultFields = convertServerResultFieldsToResultFields(serverResultFields, schema); + Object.keys(schema).forEach((fieldName) => { + if (!serverResultFields.hasOwnProperty(fieldName)) { + serverResultFields[fieldName] = {}; + } + }); + + return { + serverResultFields, + resultFields, + schema, + schemaConflicts, + ...splitResultFields(resultFields, schema), + }; + }, + clearAllFields: () => true, + resetAllFields: () => true, + updateField: (fieldName, settings) => ({ fieldName, settings }), + saving: () => true, + }), + reducers: () => ({ + dataLoading: [ + true, + { + initializeResultFields: () => false, + }, + ], + saving: [ + false, + { + initializeResultFields: () => false, + saving: () => true, + }, + ], + openModal: [ + OpenModal.None, + { + initializeResultFields: () => OpenModal.None, + closeModals: () => OpenModal.None, + resetAllFields: () => OpenModal.None, + openConfirmResetModal: () => OpenModal.ConfirmResetModal, + openConfirmSaveModal: () => OpenModal.ConfirmSaveModal, + saving: () => OpenModal.None, + }, + ], + nonTextResultFields: [ + {}, + { + initializeResultFields: (_, { nonTextResultFields }) => nonTextResultFields, + clearAllFields: (nonTextResultFields) => clearAllFields(nonTextResultFields), + resetAllFields: (nonTextResultFields) => resetAllFields(nonTextResultFields), + updateField: (nonTextResultFields, { fieldName, settings }) => + nonTextResultFields.hasOwnProperty(fieldName) + ? { ...nonTextResultFields, [fieldName]: settings } + : nonTextResultFields, + }, + ], + textResultFields: [ + {}, + { + initializeResultFields: (_, { textResultFields }) => textResultFields, + clearAllFields: (textResultFields) => clearAllFields(textResultFields), + resetAllFields: (textResultFields) => resetAllFields(textResultFields), + updateField: (textResultFields, { fieldName, settings }) => + textResultFields.hasOwnProperty(fieldName) + ? { ...textResultFields, [fieldName]: settings } + : textResultFields, + }, + ], + resultFields: [ + {}, + { + initializeResultFields: (_, { resultFields }) => resultFields, + clearAllFields: (resultFields) => clearAllFields(resultFields), + resetAllFields: (resultFields) => resetAllFields(resultFields), + updateField: (resultFields, { fieldName, settings }) => + resultFields.hasOwnProperty(fieldName) + ? { ...resultFields, [fieldName]: settings } + : resultFields, + }, + ], + serverResultFields: [ + {}, + { + initializeResultFields: (_, { serverResultFields }) => serverResultFields, + clearAllFields: (serverResultFields) => clearAllServerFields(serverResultFields), + resetAllFields: (serverResultFields) => resetAllServerFields(serverResultFields), + updateField: (serverResultFields, { fieldName, settings }) => { + return serverResultFields.hasOwnProperty(fieldName) + ? { + ...serverResultFields, + [fieldName]: convertToServerFieldResultSetting(settings), + } + : serverResultFields; + }, + }, + ], + lastSavedResultFields: [ + {}, + { + initializeResultFields: (_, { resultFields }) => resultFields, + }, + ], + schema: [ + {}, + { + initializeResultFields: (_, { schema }) => schema, + }, + ], + schemaConflicts: [ + {}, + { + initializeResultFields: (_, { schemaConflicts }) => schemaConflicts || {}, + }, + ], + }), +}); 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 new file mode 100644 index 0000000000000..da763dfe7cdc4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts @@ -0,0 +1,37 @@ +/* + * 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 enum OpenModal { + None, + ConfirmResetModal, + ConfirmSaveModal, +} +export interface ServerFieldResultSetting { + raw?: + | { + size?: number; + } + | boolean; + snippet?: + | { + size?: number; + fallback?: boolean; + } + | boolean; +} + +export type ServerFieldResultSettingObject = Record; + +export interface FieldResultSetting { + raw: boolean; + rawSize?: number; + snippet: boolean; + snippetSize?: number; + snippetFallback: boolean; +} + +export type FieldResultSettingObject = Record; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts new file mode 100644 index 0000000000000..2482ecab5892c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts @@ -0,0 +1,174 @@ +/* + * 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 { SchemaTypes } from '../../../shared/types'; + +import { + convertServerResultFieldsToResultFields, + convertToServerFieldResultSetting, + clearAllServerFields, + clearAllFields, + resetAllServerFields, + resetAllFields, + splitResultFields, +} from './utils'; + +describe('clearAllFields', () => { + it('will reset every key in an object back to an empty object', () => { + expect( + clearAllFields({ + foo: { raw: false, snippet: false, snippetFallback: false }, + bar: { raw: true, snippet: false, snippetFallback: true }, + }) + ).toEqual({ + foo: {}, + bar: {}, + }); + }); +}); + +describe('clearAllServerFields', () => { + it('will reset every key in an object back to an empty object', () => { + expect( + clearAllServerFields({ + foo: { raw: { size: 5 } }, + bar: { raw: true }, + }) + ).toEqual({ + foo: {}, + bar: {}, + }); + }); +}); + +describe('resetAllFields', () => { + it('will reset every key in an object back to a default object', () => { + expect( + resetAllFields({ + foo: { raw: false, snippet: true, snippetFallback: true }, + bar: { raw: false, snippet: true, snippetFallback: true }, + }) + ).toEqual({ + foo: { raw: true, snippet: false, snippetFallback: false }, + bar: { raw: true, snippet: false, snippetFallback: false }, + }); + }); +}); + +describe('resetAllServerFields', () => { + it('will reset every key in an object back to a default object', () => { + expect( + resetAllServerFields({ + foo: { raw: { size: 5 } }, + bar: { snippet: true }, + }) + ).toEqual({ + foo: { raw: {} }, + bar: { raw: {} }, + }); + }); +}); + +describe('convertServerResultFieldsToResultFields', () => { + it('will convert a server settings object to a format that the front-end expects', () => { + expect( + convertServerResultFieldsToResultFields( + { + foo: { + raw: { size: 5 }, + snippet: { size: 3, fallback: true }, + }, + }, + { + foo: 'text' as SchemaTypes, + } + ) + ).toEqual({ + foo: { + raw: true, + rawSize: 5, + snippet: true, + snippetFallback: true, + snippetSize: 3, + }, + }); + }); +}); + +describe('convertToServerFieldResultSetting', () => { + it('will convert a settings object to a format that the server expects', () => { + expect( + convertToServerFieldResultSetting({ + raw: true, + rawSize: 5, + snippet: true, + snippetFallback: true, + snippetSize: 3, + }) + ).toEqual({ + raw: { size: 5 }, + snippet: { size: 3, fallback: true }, + }); + }); + + it('will not include snippet or raw information if they are set to false', () => { + expect( + convertToServerFieldResultSetting({ + raw: false, + rawSize: 5, + snippet: false, + snippetFallback: true, + snippetSize: 3, + }) + ).toEqual({}); + }); + + it('will not include sizes if they are not included, or fallback if it is false', () => { + expect( + convertToServerFieldResultSetting({ + raw: true, + snippet: true, + snippetFallback: false, + }) + ).toEqual({ + raw: {}, + snippet: {}, + }); + }); +}); + +describe('splitResultFields', () => { + it('will split results based on their schema type', () => { + expect( + splitResultFields( + { + foo: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + bar: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + }, + { + foo: 'text' as SchemaTypes, + bar: 'number' as SchemaTypes, + } + ) + ).toEqual({ + nonTextResultFields: { + bar: { raw: true, rawSize: 5, snippet: false, snippetFallback: false }, + }, + textResultFields: { foo: { raw: true, rawSize: 5, snippet: false, snippetFallback: false } }, + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts new file mode 100644 index 0000000000000..0311132542d99 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts @@ -0,0 +1,117 @@ +/* + * 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 { Schema } from '../../../shared/types'; + +import { DEFAULT_FIELD_SETTINGS, DISABLED_FIELD_SETTINGS } from './constants'; +import { + FieldResultSetting, + FieldResultSettingObject, + ServerFieldResultSetting, + ServerFieldResultSettingObject, +} from './types'; + +const updateAllFields = ( + fields: FieldResultSettingObject | ServerFieldResultSettingObject, + newValue: FieldResultSetting | {} +) => { + return Object.keys(fields).reduce( + (acc, fieldName) => ({ ...acc, [fieldName]: { ...newValue } }), + {} + ); +}; + +const convertToFieldResultSetting = (serverFieldResultSetting: ServerFieldResultSetting) => { + const fieldResultSetting: FieldResultSetting = { + raw: !!serverFieldResultSetting.raw, + snippet: !!serverFieldResultSetting.snippet, + snippetFallback: !!( + serverFieldResultSetting.snippet && + typeof serverFieldResultSetting.snippet === 'object' && + serverFieldResultSetting.snippet.fallback + ), + }; + + if ( + serverFieldResultSetting.raw && + typeof serverFieldResultSetting.raw === 'object' && + serverFieldResultSetting.raw.size + ) { + fieldResultSetting.rawSize = serverFieldResultSetting.raw.size; + } + + if ( + serverFieldResultSetting.snippet && + typeof serverFieldResultSetting.snippet === 'object' && + serverFieldResultSetting.snippet.size + ) { + fieldResultSetting.snippetSize = serverFieldResultSetting.snippet.size; + } + + return fieldResultSetting; +}; + +export const clearAllFields = (fields: FieldResultSettingObject) => updateAllFields(fields, {}); + +export const clearAllServerFields = (fields: ServerFieldResultSettingObject) => + updateAllFields(fields, {}); + +export const resetAllFields = (fields: FieldResultSettingObject) => + updateAllFields(fields, DEFAULT_FIELD_SETTINGS); + +export const resetAllServerFields = (fields: ServerFieldResultSettingObject) => + updateAllFields(fields, { raw: {} }); + +export const convertServerResultFieldsToResultFields = ( + serverResultFields: ServerFieldResultSettingObject, + schema: Schema +) => { + const resultFields: FieldResultSettingObject = Object.keys(schema).reduce( + (acc: FieldResultSettingObject, fieldName: string) => ({ + ...acc, + [fieldName]: serverResultFields[fieldName] + ? convertToFieldResultSetting(serverResultFields[fieldName]) + : DISABLED_FIELD_SETTINGS, + }), + {} + ); + return resultFields; +}; + +export const convertToServerFieldResultSetting = (fieldResultSetting: FieldResultSetting) => { + const serverFieldResultSetting: ServerFieldResultSetting = {}; + if (fieldResultSetting.raw) { + serverFieldResultSetting.raw = {}; + if (fieldResultSetting.rawSize) { + serverFieldResultSetting.raw.size = fieldResultSetting.rawSize; + } + } + + if (fieldResultSetting.snippet) { + serverFieldResultSetting.snippet = {}; + if (fieldResultSetting.snippetFallback) { + serverFieldResultSetting.snippet.fallback = fieldResultSetting.snippetFallback; + } + if (fieldResultSetting.snippetSize) { + serverFieldResultSetting.snippet.size = fieldResultSetting.snippetSize; + } + } + + return serverFieldResultSetting; +}; + +export const splitResultFields = (resultFields: FieldResultSettingObject, schema: Schema) => { + const textResultFields: FieldResultSettingObject = {}; + const nonTextResultFields: FieldResultSettingObject = {}; + const keys = Object.keys(schema); + keys.forEach((fieldName) => { + (schema[fieldName] === 'text' ? textResultFields : nonTextResultFields)[fieldName] = + resultFields[fieldName]; + }); + + return { textResultFields, nonTextResultFields }; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx index c79865d25ecd7..95d7920ae0435 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { EuiButtonEmpty, EuiText } from '@elastic/eui'; +import { EuiButtonEmpty, EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { externalUrl, getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { NAV } from '../../constants'; @@ -16,23 +16,17 @@ export const WorkplaceSearchHeaderActions: React.FC = () => { if (!externalUrl.enterpriseSearchUrl) return null; return ( - <> - - {NAV.PERSONAL_DASHBOARD} - - - {NAV.SEARCH} - - + + + + {NAV.PERSONAL_DASHBOARD} + + + + + {NAV.SEARCH} + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx index 21280926d7aae..d9c2d70e78c08 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx @@ -25,7 +25,7 @@ describe('ContentSection', () => { const wrapper = shallow(); expect(wrapper.prop('data-test-subj')).toEqual('contentSection'); - expect(wrapper.prop('className')).toEqual('test content-section'); + expect(wrapper.prop('className')).toEqual('test'); expect(wrapper.find('.children')).toHaveLength(1); }); @@ -48,7 +48,7 @@ describe('ContentSection', () => { ); expect(wrapper.find(EuiSpacer).first().prop('size')).toEqual('s'); - expect(wrapper.find(EuiSpacer)).toHaveLength(1); + expect(wrapper.find(EuiSpacer)).toHaveLength(2); expect(wrapper.find('.header')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx index e606263ac6f1c..d9a4ed7eee8b8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx @@ -12,8 +12,6 @@ import { EuiSpacer } from '@elastic/eui'; import { SpacerSizeTypes } from '../../../types'; import { ViewContentHeader } from '../view_content_header'; -import './content_section.scss'; - interface ContentSectionProps { children: React.ReactNode; className?: string; @@ -35,7 +33,7 @@ export const ContentSection: React.FC = ({ headerSpacer, testSubj, }) => ( -
+
{title && ( <> @@ -44,5 +42,6 @@ export const ContentSection: React.FC = ({ )} {children} +
); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx index f0e21803a4f58..f278cda96ae83 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx @@ -19,22 +19,17 @@ export const LicenseCallout: React.FC = ({ message }) => { const title = ( <> {message}{' '} - + Explore Platinum features ); return ( -
+
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx index ee079970a5ebb..4d980ca2f5313 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx @@ -14,16 +14,10 @@ import { EuiIcon } from '@elastic/eui'; import { SourceIcon } from './'; describe('SourceIcon', () => { - it('renders unwrapped icon', () => { + it('renders', () => { const wrapper = shallow(); expect(wrapper.find(EuiIcon)).toHaveLength(1); expect(wrapper.find('.user-group-source')).toHaveLength(0); }); - - it('renders wrapped icon', () => { - const wrapper = shallow(); - - expect(wrapper.find('.wrapped-icon')).toHaveLength(1); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx index 1d1462542a3f6..34d6c2401b300 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx @@ -11,33 +11,15 @@ import { camelCase } from 'lodash'; import { EuiIcon, IconSize } from '@elastic/eui'; -import './source_icon.scss'; - import { images } from '../assets/source_icons'; interface SourceIconProps { serviceType: string; name: string; className?: string; - wrapped?: boolean; size?: IconSize; } -export const SourceIcon: React.FC = ({ - name, - serviceType, - className, - wrapped, - size = 'xxl', -}) => { - const icon = ( - - ); - return wrapped ? ( -
- {icon} -
- ) : ( - <>{icon} - ); -}; +export const SourceIcon: React.FC = ({ name, serviceType, className, size }) => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.scss deleted file mode 100644 index fb8a47d134269..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.scss +++ /dev/null @@ -1,15 +0,0 @@ -.source-row { - &__icon { - width: 24px; - height: 24px; - } - - &__name { - font-weight: 500; - } - - &__actions a { - opacity: 1.0; - pointer-events: auto; - } -} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx index 9661471bb1dd7..b3ce0a01d5dd4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx @@ -73,7 +73,7 @@ describe('SourceRow', () => { }; const wrapper = shallow(); - expect(wrapper.find('.source-row__document-count').contains('Remote')).toBeTruthy(); + expect(wrapper.find('[data-test-subj="SourceDocumentCount"]').contains('Remote')).toBeTruthy(); }); it('renders details link', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx index a6b2878de6449..38d7945ca1753 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx @@ -7,7 +7,6 @@ import React from 'react'; -import classNames from 'classnames'; // Prefer importing entire lodash library, e.g. import { get } from "lodash" // eslint-disable-next-line no-restricted-imports import _kebabCase from 'lodash/kebabCase'; @@ -35,8 +34,6 @@ import { import { ContentSourceDetails } from '../../../types'; import { SourceIcon } from '../source_icon'; -import './source_row.scss'; - const CREDENTIALS_INVALID_ERROR_REASON = 1; export interface ISourceRow { @@ -72,15 +69,6 @@ export const SourceRow: React.FC = ({ const showFix = isOrganization && hasError && allowsReauth && errorReason === CREDENTIALS_INVALID_ERROR_REASON; - const rowClass = classNames( - 'source-row', - { 'content-section--disabled': !searchable }, - { 'source-row source-row--error': hasError } - ); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const imageClass = classNames('source-row__icon', { 'source-row__icon--loading': isIndexing }); - const fixLink = ( = ({ ); return ( - + = ({ responsive={false} > - - - - {name} + + {name} @@ -138,17 +120,13 @@ export const SourceRow: React.FC = ({ )} - + {statusMessage} - + {isFederatedSource ? remoteTooltip : parseInt(documentCount, 10).toLocaleString('en-US')} {onSearchableToggle && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx index 66e7e2e752a1e..9bc3d6ec2f1f4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx @@ -27,7 +27,7 @@ export const SourcesTable: React.FC = ({ if (onSearchableToggle) headerItems.push('Searchable'); return ( - + {sources.map((source) => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx index 2ecb3c98565b7..a4910f3a68ea2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx @@ -34,11 +34,16 @@ export const AddSourceHeader: React.FC = ({ responsive={false} > - + -

+

{name}

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index 372187485f277..8819367cacd1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -126,7 +126,7 @@ export const AddSourceList: React.FC = () => { - + = ({ sour title={name} description={<>} isDisabled={disabled} - icon={ - - } + icon={} /> ); @@ -79,7 +73,7 @@ export const AvailableSourcesList: React.FC = ({ sour }; const visibleSources = ( - + {sources.map((source, i) => ( {getSourceCard(source)} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx index 1d4f1f2fca980..8edef425f414c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx @@ -48,7 +48,7 @@ export const ConfigCompleted: React.FC = ({ header, privateSourcesEnabled, }) => ( -
+ <> {header} = ({ to={getSourcesPath(ADD_SOURCE_PATH, true)} fill={accountContextOnly} color={accountContextOnly ? 'primary' : undefined} - className="eui-textNoWrap" > {CONFIG_COMPLETED_CONFIGURE_NEW_BUTTON} @@ -148,7 +147,6 @@ export const ConfigCompleted: React.FC = ({ = ({ )} -
+ ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx index 917886d69bd19..8a1cdf0b84274 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx @@ -43,7 +43,7 @@ export const ConfigurationIntro: React.FC = ({ advanceStep, header, }) => ( -
+ <> {header} = ({ -
+ ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx index 901c4fc3e707b..b142ddcf4937e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx @@ -48,7 +48,7 @@ export const ConfigureCustom: React.FC = ({ setCustomSourceNameValue(e.target.value); return ( -
+ <> {header}
@@ -93,6 +93,6 @@ export const ConfigureCustom: React.FC = ({ -
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx index 69a2fbd1495c7..ba92e5d790245 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx @@ -99,9 +99,9 @@ export const ConfigureOauth: React.FC = ({ name, onFormCrea ); return ( -
+ <> {header} {sectionLoading ? : configfieldsForm} -
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx index 5f64913410d4c..ac19043a30ca6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx @@ -79,16 +79,12 @@ export const ConfiguredSourcesList: React.FC = ({ responsive={false} > - +

- {name}  + {name} {!connected && !accountContextOnly && isOrganization && diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index 2290a65912797..a34641784b162 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -263,30 +263,28 @@ export const ConnectInstance: React.FC = ({ ); return ( -
-
- - - - - {header} - - - - - + + + + + + {header} - - {formFields} - - - -
+ + + + + + + {formFields} + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx index 61682dbb87d58..f57118b952eac 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx @@ -49,7 +49,7 @@ export const ReAuthenticate: React.FC = ({ name, header }) }; return ( -
+ <> {header}
@@ -86,6 +86,6 @@ export const ReAuthenticate: React.FC = ({ name, header }) -
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx index 5aae4b352a1fb..1bf8239a6b399 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx @@ -60,7 +60,7 @@ export const SaveCustom: React.FC = ({ isOrganization, header, }) => ( -
+ <> {header} @@ -205,5 +205,5 @@ export const SaveCustom: React.FC = ({ -
+ ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx index aab16c9114e8a..ad16260b1de7c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx @@ -187,12 +187,7 @@ export const SourceFeatures: React.FC = ({ features, objTy {includedFeatures.map((featureId, i) => ( - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index 681c7a21463c1..e39a8d17e406c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -125,7 +125,7 @@ export const DisplaySettings: React.FC = ({ tabId }) => { onTabClick={onSelectedTabChanged} /> ) : ( - + {DISPLAY_SETTINGS_EMPTY_TITLE}

} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index 786184943e317..dc925e21460da 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -116,7 +116,7 @@ export const Overview: React.FC = () => { const emptyState = ( <> - + {SOURCES_NO_CONTENT_TITLE}} iconType="documents" @@ -127,12 +127,10 @@ export const Overview: React.FC = () => { ); return ( -
-
- -

{CONTENT_SUMMARY_TITLE}

-
-
+ <> + +

{CONTENT_SUMMARY_TITLE}

+
{!summary && } {!!summary && @@ -157,7 +155,7 @@ export const Overview: React.FC = () => { ))} -
+ ); }; @@ -165,7 +163,7 @@ export const Overview: React.FC = () => { const emptyState = ( <> - + {EMPTY_ACTIVITY_TITLE}} iconType="clock" @@ -213,15 +211,13 @@ export const Overview: React.FC = () => { ); return ( -
-
- -

{RECENT_ACTIVITY_TITLE}

-
-
+ <> + +

{RECENT_ACTIVITY_TITLE}

+
{activities.length === 0 ? emptyState : activitiesTable} -
+ ); }; @@ -234,11 +230,7 @@ export const Overview: React.FC = () => { {groups.map((group, index) => ( - + {group.name} @@ -306,7 +298,7 @@ export const Overview: React.FC = () => {

{DOCUMENT_PERMISSIONS_TITLE}

- + @@ -455,7 +447,7 @@ export const Overview: React.FC = () => { - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx index 77d1002a9ad26..f31f7049ebf36 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx @@ -140,7 +140,7 @@ export const Schema: React.FC = () => { ) : ( - + {SCHEMA_EMPTY_SCHEMA_TITLE}} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx index 1a6d97bbf75ba..cc086f9c829d9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -106,7 +106,7 @@ export const SourceContent: React.FC = () => { const isCustomSource = serviceType === CUSTOM_SERVICE_TYPE; const emptyState = ( - + {emptyMessage}} iconType="documents" diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx index 25c78afbe4e05..1c3c44887946a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -35,18 +35,13 @@ export const SourceInfoCard: React.FC = ({ }) => ( - + - + -
{sourceName}
+
{sourceName}
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 437b8010d6891..f142567fb621f 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 @@ -10,11 +10,6 @@ .source-card-configured { padding: 8px; - &__icon { - width: 2em; - height: 2em; - } - &__not-connected-tooltip { position: relative; top: 3px; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx index 247df5556ada0..1ef44a5f26ae0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx @@ -62,7 +62,7 @@ export const SourcesView: React.FC = ({ children }) => { gutterSize="s" > - + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.sourcesView.modal.heading', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx index df7435bd25461..bee8b4e632a64 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx @@ -106,7 +106,7 @@ export const GroupSourcePrioritization: React.FC = () => { const hasSources = contentSources.length > 0; const zeroState = ( - + { ); const sourceTable = ( - + {SOURCE_TABLE_HEADER} {PRIORITY_TABLE_HEADER} @@ -143,14 +143,12 @@ export const GroupSourcePrioritization: React.FC = () => { - - - - {name} + + {name} - + { min={1} max={10} step={1} + showInput value={activeSourcePriorities[id]} onChange={(e: ChangeEvent | MouseEvent) => handleSliderChange(id, e) } /> - -
- {activeSourcePriorities[id]} -
-
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx index 97739e46caba4..26d56c7435d00 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx @@ -7,6 +7,8 @@ import React, { useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + import { SourceIcon } from '../../../components/shared/source_icon'; import { MAX_TABLE_ROW_ICONS } from '../../../constants'; import { ContentSource } from '../../../types'; @@ -26,9 +28,13 @@ export const GroupSources: React.FC = ({ groupSources }) => { return ( <> - {visibleSources.map((source, index) => ( - - ))} + + {visibleSources.map((source, index) => ( + + + + ))} + {hiddenSources.length > 0 && ( { return ( <> - + {users.slice(firstItem, lastItem + 1).map((user: User) => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx index 95292116cba05..deaf223afa6b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx @@ -75,7 +75,7 @@ export const GroupsTable: React.FC<{}> = () => { <> {showPagination ? : clearFiltersLink} - + {GROUP_TABLE_HEADER} {SOURCES_TABLE_HEADER} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx index e2da597a83598..7983b3a13f4cd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx @@ -22,7 +22,7 @@ interface SourceOptionItemProps { export const SourceOptionItem: React.FC = ({ source }) => ( - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx index 0f8f4b6def46c..9242bd8b6fbdd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx @@ -54,13 +54,8 @@ export const Overview: React.FC = () => { initializeOverview(); }, [initializeOverview]); - // TODO: Remove div wrapper once the Overview page is using the full Layout if (dataLoading) { - return ( -
- -
- ); + return ; } const hideOnboarding = hasUsers && hasOrgSources && isOldAccount && orgName !== defaultOrgName; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx index 4658379cd40c7..cf402f4525f9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx @@ -163,7 +163,7 @@ export const RoleMapping: React.FC = ({ isNew }) => {

{GROUP_ASSIGNMENT_TITLE}

-
+
= ({ const panelDisabled = !isEnabled || !hasPlatinumLicense; const sectionDisabled = !sectionEnabled; - const panelClass = classNames('euiPanel--outline euiPanel--noShadow', { - 'euiPanel--disabled': panelDisabled, - }); - const tableClass = classNames({ 'euiTable--disabled': sectionDisabled }); const emptyState = ( <> - + {isRemote ? REMOTE_SOURCES_EMPTY_TABLE_TITLE : STANDARD_SOURCES_EMPTY_TABLE_TITLE} @@ -175,7 +171,12 @@ export const PrivateSourcesTable: React.FC = ({ ); return ( - + {sectionHeading} {hasSources && sourcesTable} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx index 669015794baef..1248d9caf7e7d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx @@ -74,10 +74,6 @@ export const Security: React.FC = () => { if (dataLoading) return ; - const panelClass = classNames('euiPanel--noShadow', { - 'euiPanel--disabled': !hasPlatinumLicense, - }); - const savePrivateSources = () => { saveSourceRestrictions(); hideConfirmModal(); @@ -116,7 +112,13 @@ export const Security: React.FC = () => { ); const allSourcesToggle = ( - + { - + - - - {name} -    - {accountContextOnly && {PRIVATE_SOURCE}} - + + + {name} + + {accountContextOnly && {PRIVATE_SOURCE}} + + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx index 3f2e55d23722c..929508bdf7b23 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx @@ -92,12 +92,7 @@ export const OauthApplication: React.FC = () => { }; const licenseModal = ( - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx index 47a24e7912c3c..5372917b3eba2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx @@ -18,6 +18,11 @@ import { AddSourceHeader } from '../../content_sources/components/add_source/add import { AddSourceLogic } from '../../content_sources/components/add_source/add_source_logic'; import { SaveConfig } from '../../content_sources/components/add_source/save_config'; import { staticSourceData } from '../../content_sources/source_data'; +import { + CONFIRM_REMOVE_CONFIG_TITLE, + CONFIRM_REMOVE_CONFIG_CONFIRM_BUTTON_TEXT, + CONFIRM_REMOVE_CONFIG_CANCEL_BUTTON_TEXT, +} from '../constants'; import { SettingsLogic } from '../settings_logic'; interface SourceConfigProps { @@ -60,6 +65,9 @@ export const SourceConfig: React.FC = ({ sourceIndex }) => { onConfirm={() => deleteSourceConfig(serviceType, name)} onCancel={hideConfirmModal} buttonColor="danger" + title={CONFIRM_REMOVE_CONFIG_TITLE} + confirmButtonText={CONFIRM_REMOVE_CONFIG_CONFIRM_BUTTON_TEXT} + cancelButtonText={CONFIRM_REMOVE_CONFIG_CANCEL_BUTTON_TEXT} > {i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.settings.confirmRemoveConfig.message', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/constants.ts new file mode 100644 index 0000000000000..b40edc1d0d1bd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/constants.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 { i18n } from '@kbn/i18n'; + +export const CONFIRM_REMOVE_CONFIG_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.confirmRemoveConfigTitle', + { + defaultMessage: 'Remove configuration', + } +); + +export const CONFIRM_REMOVE_CONFIG_CONFIRM_BUTTON_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.confirmRemoveConfigConfirmButtonText', + { + defaultMessage: 'Remove', + } +); + +export const CONFIRM_REMOVE_CONFIG_CANCEL_BUTTON_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.confirmRemoveConfigCancelButtonText', + { + defaultMessage: 'Cancel', + } +); 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 b56c5880dba43..6638aa82b32ad 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 @@ -859,6 +859,10 @@ export function registerOauthConnectorParamsRoute({ kibana_host: schema.string(), code: schema.maybe(schema.string()), session_state: schema.maybe(schema.string()), + authuser: schema.maybe(schema.string()), + prompt: schema.maybe(schema.string()), + hd: schema.maybe(schema.string()), + scope: schema.maybe(schema.string()), state: schema.string(), oauth_token: schema.maybe(schema.string()), oauth_verifier: schema.maybe(schema.string()), diff --git a/x-pack/plugins/fleet/common/types/models/settings.ts b/x-pack/plugins/fleet/common/types/models/settings.ts index 56557fb6703b4..bb345a67bec41 100644 --- a/x-pack/plugins/fleet/common/types/models/settings.ts +++ b/x-pack/plugins/fleet/common/types/models/settings.ts @@ -8,8 +8,6 @@ import type { SavedObjectAttributes } from 'src/core/public'; export interface BaseSettings { - agent_auto_upgrade: boolean; - package_auto_upgrade: boolean; kibana_urls: string[]; kibana_ca_sha256?: string; has_seen_add_data_notice?: boolean; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx index c668097cdb47e..146f40cd75d49 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx @@ -20,7 +20,6 @@ import { EuiFlyoutFooter, EuiForm, EuiFormRow, - EuiRadioGroup, EuiComboBox, EuiCodeEditor, } from '@elastic/eui'; @@ -171,73 +170,6 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { const body = ( - {}} - legend={{ - children: ( - -

- -

-
- ), - }} - /> - - {}} - legend={{ - children: ( - -

- -

-
- ), - }} - /> -

{agentPolicy.name || agentPolicy.id} diff --git a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts index 245ef4afdeb11..558a9a8afbb0b 100644 --- a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts @@ -52,19 +52,20 @@ export const postBulkAgentsUnenrollHandler: RequestHandler< body: { message: 'Requires Gold license' }, }); } + const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asInternalUser; - const unenrollAgents = - request.body?.force === true ? AgentService.forceUnenrollAgents : AgentService.unenrollAgents; + const agentOptions = Array.isArray(request.body.agents) + ? { agentIds: request.body.agents } + : { kuery: request.body.agents }; try { - if (Array.isArray(request.body.agents)) { - await unenrollAgents(soClient, esClient, { agentIds: request.body.agents }); - } else { - await unenrollAgents(soClient, esClient, { kuery: request.body.agents }); - } - + await AgentService.unenrollAgents(soClient, esClient, { + ...agentOptions, + force: request.body?.force, + }); const body: PostBulkAgentUnenrollResponse = {}; + return response.ok({ body }); } catch (error) { return defaultIngestErrorHandler({ error, response }); diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 923f4704aa652..b3edf9e449e3d 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -39,6 +39,8 @@ import { migratePackagePolicyToV7120, } from './migrations/to_v7_12_0'; +import { migrateSettingsToV7130 } from './migrations/to_v7_13_0'; + /* * Saved object types and mappings * @@ -57,8 +59,6 @@ const getSavedObjectTypes = ( }, mappings: { properties: { - agent_auto_upgrade: { type: 'keyword' }, - package_auto_upgrade: { type: 'keyword' }, kibana_urls: { type: 'keyword' }, kibana_ca_sha256: { type: 'keyword' }, has_seen_add_data_notice: { type: 'boolean', index: false }, @@ -66,6 +66,7 @@ const getSavedObjectTypes = ( }, migrations: { '7.10.0': migrateSettingsToV7100, + '7.13.0': migrateSettingsToV7130, }, }, [AGENT_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts new file mode 100644 index 0000000000000..5c660d4309ac7 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectMigrationFn } from 'kibana/server'; + +import type { Settings } from '../../types'; + +export const migrateSettingsToV7130: SavedObjectMigrationFn< + Settings & { + package_auto_upgrade: string; + agent_auto_upgrade: string; + }, + Settings +> = (settingsDoc) => { + // @ts-expect-error + delete settingsDoc.attributes.package_auto_upgrade; + // @ts-expect-error + delete settingsDoc.attributes.agent_auto_upgrade; + + return settingsDoc; +}; diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index 14f9aa46e9fa6..8cf7396eaa8de 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -56,14 +56,18 @@ export async function unenrollAgent( export async function unenrollAgents( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - options: GetAgentsOptions + options: GetAgentsOptions & { force?: boolean } ) { + // start with all agents specified const agents = await getAgents(esClient, options); - // Filter to agents that are not already unenrolled, or unenrolling - const agentsEnrolled = agents.filter( - (agent) => !agent.unenrollment_started_at && !agent.unenrolled_at - ); + // Filter to those not already unenrolled, or unenrolling + const agentsEnrolled = agents.filter((agent) => { + if (options.force) { + return !agent.unenrolled_at; + } + return !agent.unenrollment_started_at && !agent.unenrolled_at; + }); // And which are allowed to unenroll const settled = await Promise.allSettled( agentsEnrolled.map((agent) => @@ -71,30 +75,59 @@ export async function unenrollAgents( ) ); const agentsToUpdate = agentsEnrolled.filter((_, index) => settled[index].status === 'fulfilled'); - const now = new Date().toISOString(); - // Create unenroll action for each agent - await bulkCreateAgentActions( - soClient, - esClient, - agentsToUpdate.map((agent) => ({ - agent_id: agent.id, - created_at: now, - type: 'UNENROLL', - })) - ); + if (options.force) { + // Get all API keys that need to be invalidated + const apiKeys = agentsToUpdate.reduce((keys, agent) => { + if (agent.access_api_key_id) { + keys.push(agent.access_api_key_id); + } + if (agent.default_api_key_id) { + keys.push(agent.default_api_key_id); + } + + return keys; + }, []); + + // Invalidate all API keys + if (apiKeys.length) { + await APIKeyService.invalidateAPIKeys(soClient, apiKeys); + } + // Update the necessary agents + return bulkUpdateAgents( + esClient, + agentsToUpdate.map((agent) => ({ + agentId: agent.id, + data: { + active: false, + unenrolled_at: now, + }, + })) + ); + } else { + // Create unenroll action for each agent + await bulkCreateAgentActions( + soClient, + esClient, + agentsToUpdate.map((agent) => ({ + agent_id: agent.id, + created_at: now, + type: 'UNENROLL', + })) + ); - // Update the necessary agents - return bulkUpdateAgents( - esClient, - agentsToUpdate.map((agent) => ({ - agentId: agent.id, - data: { - unenrollment_started_at: now, - }, - })) - ); + // Update the necessary agents + return bulkUpdateAgents( + esClient, + agentsToUpdate.map((agent) => ({ + agentId: agent.id, + data: { + unenrollment_started_at: now, + }, + })) + ); + } } export async function forceUnenrollAgent( @@ -118,41 +151,3 @@ export async function forceUnenrollAgent( unenrolled_at: new Date().toISOString(), }); } - -export async function forceUnenrollAgents( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - options: GetAgentsOptions -) { - // Filter to agents that are not already unenrolled - const agents = await getAgents(esClient, options); - const agentsToUpdate = agents.filter((agent) => !agent.unenrolled_at); - const now = new Date().toISOString(); - const apiKeys: string[] = []; - - // Get all API keys that need to be invalidated - agentsToUpdate.forEach((agent) => { - if (agent.access_api_key_id) { - apiKeys.push(agent.access_api_key_id); - } - if (agent.default_api_key_id) { - apiKeys.push(agent.default_api_key_id); - } - }); - - // Invalidate all API keys - if (apiKeys.length) { - APIKeyService.invalidateAPIKeys(soClient, apiKeys); - } - // Update the necessary agents - return bulkUpdateAgents( - esClient, - agentsToUpdate.map((agent) => ({ - agentId: agent.id, - data: { - active: false, - unenrolled_at: now, - }, - })) - ); -} diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 3a322ebb7dd9d..03348a2fcc4bb 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -82,8 +82,6 @@ export function createDefaultSettings(): BaseSettings { }); return { - agent_auto_upgrade: true, - package_auto_upgrade: true, kibana_urls: [cloudUrl || flagsUrl || defaultUrl].flat(), }; } diff --git a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts index fa885d45c1115..9bbebbe86ccaa 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts @@ -13,8 +13,6 @@ export const GetSettingsRequestSchema = {}; export const PutSettingsRequestSchema = { body: schema.object({ - agent_auto_upgrade: schema.maybe(schema.boolean()), - package_auto_upgrade: schema.maybe(schema.boolean()), kibana_urls: schema.maybe( schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { validate: (value) => { diff --git a/x-pack/plugins/infra/server/types.ts b/x-pack/plugins/infra/server/types.ts index 5cae015861946..1c51a5549cb41 100644 --- a/x-pack/plugins/infra/server/types.ts +++ b/x-pack/plugins/infra/server/types.ts @@ -5,8 +5,7 @@ * 2.0. */ -import type { RequestHandlerContext } from 'src/core/server'; -import type { SearchRequestHandlerContext } from '../../../../src/plugins/data/server'; +import type { DataRequestHandlerContext } from '../../../../src/plugins/data/server'; import { MlPluginSetup } from '../../ml/server'; export type MlSystem = ReturnType; @@ -27,7 +26,6 @@ export type InfraRequestHandlerContext = InfraMlRequestHandlerContext & /** * @internal */ -export interface InfraPluginRequestHandlerContext extends RequestHandlerContext { +export interface InfraPluginRequestHandlerContext extends DataRequestHandlerContext { infra: InfraRequestHandlerContext; - search: SearchRequestHandlerContext; } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/on_failure_processors_title.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/on_failure_processors_title.tsx index ec143ac31438c..658ffe08607d8 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/on_failure_processors_title.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/on_failure_processors_title.tsx @@ -32,9 +32,7 @@ export const OnFailureProcessorsTitle: FunctionComponent = () => { values={{ learnMoreLink: ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx index abe4eb0fa5916..03bdc2ceb9579 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx @@ -44,7 +44,15 @@ interface Props { /** * Validation to be applied to every text item */ - textValidation?: ValidationFunc; + textValidations?: Array>; + /** + * Serializer to be applied to every text item + */ + textSerializer?: (v: string) => O; + /** + * Deserializer to be applied to every text item + */ + textDeserializer?: (v: unknown) => string; } const i18nTexts = { @@ -63,7 +71,9 @@ function DragAndDropTextListComponent({ onAdd, onRemove, addLabel, - textValidation, + textValidations, + textDeserializer, + textSerializer, }: Props): JSX.Element { const [droppableId] = useState(() => uuid.v4()); const [firstItemId] = useState(() => uuid.v4()); @@ -133,9 +143,11 @@ function DragAndDropTextListComponent({ path={item.path} config={{ - validations: textValidation - ? [{ validator: textValidation }] + validations: textValidations + ? textValidations.map((validator) => ({ validator })) : undefined, + deserializer: textDeserializer, + serializer: textSerializer, }} readDefaultValueOnForm={!item.isNew} > diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx index 6652ad277cc26..3864581317e38 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx @@ -22,7 +22,7 @@ import { import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; -import { EDITOR_PX_HEIGHT, from } from './shared'; +import { EDITOR_PX_HEIGHT, from, to, isJSONStringValidator } from './shared'; const { emptyField } = fieldValidators; @@ -34,6 +34,8 @@ const getFieldsConfig = (esDocUrl: string): Record => { label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldLabel', { defaultMessage: 'Pattern', }), + deserializer: to.escapeBackslashes, + serializer: from.unescapeBackslashes, helpText: ( => { ) ), }, + { + validator: isJSONStringValidator, + }, ], }, /* Optional field config */ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.tsx index f15441ea1f92b..ae2d341c58c30 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/grok.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { flow } from 'lodash'; import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; @@ -22,7 +23,7 @@ import { XJsonEditor, DragAndDropTextList } from '../field_components'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; -import { FieldsConfig, to, from, EDITOR_PX_HEIGHT } from './shared'; +import { FieldsConfig, to, from, EDITOR_PX_HEIGHT, isJSONStringValidator } from './shared'; const { isJsonField, emptyField } = fieldValidators; @@ -46,7 +47,10 @@ const patternsValidation: ValidationFunc = ({ value, f } }; -const patternValidation = emptyField(valueRequiredMessage); +const patternValidations: Array> = [ + emptyField(valueRequiredMessage), + isJSONStringValidator, +]; const fieldsConfig: FieldsConfig = { /* Required field configs */ @@ -54,6 +58,8 @@ const fieldsConfig: FieldsConfig = { label: i18n.translate('xpack.ingestPipelines.pipelineEditor.grokForm.patternsFieldLabel', { defaultMessage: 'Patterns', }), + deserializer: flow(String, to.escapeBackslashes), + serializer: from.unescapeBackslashes, helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.grokForm.patternsHelpText', { defaultMessage: 'Grok expressions used to match and extract named capture groups. Uses the first matching expression.', @@ -133,7 +139,9 @@ export const Grok: FunctionComponent = () => { onAdd={addItem} onRemove={removeItem} addLabel={i18nTexts.addPatternLabel} - textValidation={patternValidation} + textValidations={patternValidations} + textDeserializer={fieldsConfig.patterns?.deserializer} + textSerializer={fieldsConfig.patterns?.serializer} /> ); }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx index edfa59ea80281..11d06f3cca6fb 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { flow } from 'lodash'; import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; @@ -12,7 +13,7 @@ import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../.. import { TextEditor } from '../field_components'; -import { EDITOR_PX_HEIGHT, FieldsConfig } from './shared'; +import { EDITOR_PX_HEIGHT, FieldsConfig, from, to, isJSONStringValidator } from './shared'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { TargetField } from './common_fields/target_field'; @@ -26,7 +27,8 @@ const fieldsConfig: FieldsConfig = { label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldLabel', { defaultMessage: 'Pattern', }), - deserializer: String, + deserializer: flow(String, to.escapeBackslashes), + serializer: from.unescapeBackslashes, helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldHelpText', { defaultMessage: 'Regular expression used to match substrings in the field.', }), @@ -38,6 +40,9 @@ const fieldsConfig: FieldsConfig = { }) ), }, + { + validator: isJSONStringValidator, + }, ], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.test.ts new file mode 100644 index 0000000000000..4b01f22a9383d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { from, to } from './shared'; + +describe('shared', () => { + describe('deserialization helpers', () => { + // This is the text that will be passed to the text input + test('to.escapeBackslashes', () => { + // this input loaded from the server + const input1 = 'my\ttab'; + expect(to.escapeBackslashes(input1)).toBe('my\\ttab'); + + // this input loaded from the server + const input2 = 'my\\ttab'; + expect(to.escapeBackslashes(input2)).toBe('my\\\\ttab'); + + // this input loaded from the server + const input3 = '\t\n\rOK'; + expect(to.escapeBackslashes(input3)).toBe('\\t\\n\\rOK'); + + const input4 = `%{clientip} %{ident} %{auth} [%{@timestamp}] \"%{verb} %{request} HTTP/%{httpversion}\" %{status} %{size}`; + expect(to.escapeBackslashes(input4)).toBe( + '%{clientip} %{ident} %{auth} [%{@timestamp}] \\"%{verb} %{request} HTTP/%{httpversion}\\" %{status} %{size}' + ); + }); + }); + + describe('serialization helpers', () => { + test('from.unescapeBackslashes', () => { + // user typed in "my\ttab" + const input1 = 'my\\ttab'; + expect(from.unescapeBackslashes(input1)).toBe('my\ttab'); + + // user typed in "my\\tab" + const input2 = 'my\\\\ttab'; + expect(from.unescapeBackslashes(input2)).toBe('my\\ttab'); + + // user typed in "\t\n\rOK" + const input3 = '\\t\\n\\rOK'; + expect(from.unescapeBackslashes(input3)).toBe('\t\n\rOK'); + + const input5 = `%{clientip} %{ident} %{auth} [%{@timestamp}] \\"%{verb} %{request} HTTP/%{httpversion}\\" %{status} %{size}`; + expect(from.unescapeBackslashes(input5)).toBe( + `%{clientip} %{ident} %{auth} [%{@timestamp}] \"%{verb} %{request} HTTP/%{httpversion}\" %{status} %{size}` + ); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts index 399da3c05c783..bafba412c767f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { FunctionComponent } from 'react'; +import type { FunctionComponent } from 'react'; import * as rt from 'io-ts'; +import { i18n } from '@kbn/i18n'; import { isRight } from 'fp-ts/lib/Either'; -import { FieldConfig } from '../../../../../../shared_imports'; +import { FieldConfig, ValidationFunc } from '../../../../../../shared_imports'; export const arrayOfStrings = rt.array(rt.string); @@ -36,6 +37,17 @@ export const to = { arrayOfStrings: (v: unknown): string[] => isArrayOfStrings(v) ? v : typeof v === 'string' && v.length ? [v] : [], jsonString: (v: unknown) => (v ? JSON.stringify(v, null, 2) : '{}'), + /** + * Useful when deserializing strings that will be rendered inside of text areas or text inputs. We want + * a string like: "my\ttab" to render the same, not to render as "mytab". + */ + escapeBackslashes: (v: unknown) => { + if (typeof v === 'string') { + const s = JSON.stringify(v); + return s.slice(1, s.length - 1); + } + return v; + }, }; /** @@ -69,6 +81,41 @@ export const from = { optionalArrayOfStrings: (v: string[]) => (v.length ? v : undefined), undefinedIfValue: (value: unknown) => (v: boolean) => (v === value ? undefined : v), emptyStringToUndefined: (v: unknown) => (v === '' ? undefined : v), + /** + * Useful when serializing user input from a