diff --git a/docs/user/alerting/images/es-query-rule-action-query-matched.png b/docs/user/alerting/images/es-query-rule-action-query-matched.png index cafa6e82e2ab2..a754a9c667996 100644 Binary files a/docs/user/alerting/images/es-query-rule-action-query-matched.png and b/docs/user/alerting/images/es-query-rule-action-query-matched.png differ diff --git a/docs/user/alerting/images/es-query-rule-action-summary.png b/docs/user/alerting/images/es-query-rule-action-summary.png index 1e098d77fc5f3..aa6857f72dc96 100644 Binary files a/docs/user/alerting/images/es-query-rule-action-summary.png and b/docs/user/alerting/images/es-query-rule-action-summary.png differ diff --git a/docs/user/alerting/images/es-query-rule-action-variables.png b/docs/user/alerting/images/es-query-rule-action-variables.png index 685f455b986ab..dbb30a7a3b58b 100644 Binary files a/docs/user/alerting/images/es-query-rule-action-variables.png and b/docs/user/alerting/images/es-query-rule-action-variables.png differ diff --git a/docs/user/alerting/rule-types/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc index 2ea5e1dc76af2..d9e768db962cf 100644 --- a/docs/user/alerting/rule-types/es-query.asciidoc +++ b/docs/user/alerting/rule-types/es-query.asciidoc @@ -45,7 +45,7 @@ For example: If you use {kibana-ref}/kuery-query.html[KQL] or {kibana-ref}/lucene-query.html[Lucene], you must specify a data view then define a text-based query. For example, `http.request.referrer: "https://example.com"`. -preview:["Do not use {esql} on production environments. This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features."] If you use {ref}/esql.html[ES|QL], you must provide a source command followed by an optional series of processing commands, separated by pipe characters (|). +If you use {ref}/esql.html[ES|QL], you must provide a source command followed by an optional series of processing commands, separated by pipe characters (|). added:[8.16.0] For example: [source,sh] @@ -101,7 +101,7 @@ For example: image::user/alerting/images/rule-types-es-query-valid.png[Test {es} query returns number of matches when valid] // NOTE: This is an autogenerated screenshot. Do not edit it directly. -preview:[] If you use an ES|QL query, a table is displayed. For example: +If you use an ES|QL query, a table is displayed. For example: [role="screenshot"] image::user/alerting/images/rule-types-esql-query-valid.png[Test ES|QL query returns a table when valid] diff --git a/docs/user/dashboard/create-dashboards.asciidoc b/docs/user/dashboard/create-dashboards.asciidoc index 48fba9a65d3a5..b07b4e88a684a 100644 --- a/docs/user/dashboard/create-dashboards.asciidoc +++ b/docs/user/dashboard/create-dashboards.asciidoc @@ -26,22 +26,24 @@ TIP: If you don't have data at hand and still want to explore dashboards, you ca //To make your dashboard experience as good as possible for you and users who will view it, check the <>. -. Open the *Dashboard* page in {kib}. +. Open the *Dashboards* page in {kib}. . Select *Create dashboard* to start with an empty dashboard. + When you create a dashboard, you are automatically in edit mode and can make changes to the dashboard. [[create-panels-with-lens]] -. Add content to the dashboard. You have several options covered in more details in the <>: +. Add content to the dashboard. You have several options covered in more detail in the <>: ** <>. This option is a shortcut to create a chart using **Lens**, the default visualization editor in {kib}. ** <>. Choose one of the available panels to add and configure content to your dashboard. ** **Add from library**. Select existing content that has already been configured and saved to the **Visualize Library**. ** <>. Add controls to help filter the content of your dashboard. - ++ +[role="screenshot"] +image::images/add_content_to_dashboard_8.15.0.png[Options to add content to your dashboard, width=70%] . Organize your dashboard by <>. [[add-dashboard-settings]] . Define the main settings of your dashboard from the *Settings* menu located in the toolbar. -.. Meaningful title, description, and <> allow you to find the dashboard quickly later when browsing your list of dashboard or using the {kib} search bar. +.. A meaningful title, description, and <> allow you to find the dashboard quickly later when browsing your list of dashboards or using the {kib} search bar. .. Additional display options allow you unify the look and feel of the dashboard's panels: *** *Store time with dashboard* — Saves the specified time filter. @@ -49,16 +51,19 @@ When you create a dashboard, you are automatically in edit mode and can make cha *** *Show panel titles* — Displays the titles in the panel headers. *** *Sync color palettes across panels* — Applies the same color palette to all panels on the dashboard. *** *Sync cursor across panels* — When you hover your cursor over a time series chart or a heatmap, the cursor on all other related dashboard charts automatically appears. -*** *Sync tooltips across panels* — When you hover your cursor over a *Lens* chart, the tooltips on all other related dashboard charts automatically appears. +*** *Sync tooltips across panels* — When you hover your cursor over a *Lens* chart, the tooltips on all other related dashboard charts automatically appear. -.. Click *Apply*. +.. Click *Apply*. ++ +[role="screenshot"] +image::https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt532d6c9ca72817d6/66f31b1f80b55f3a20e1a329/dashboard_settings_8.15.0.gif[Change and apply dashboard settings, width 30%] -. **Save** Save the dashboard. +. Click **Save** to save the dashboard. [[open-the-dashboard]] == Edit a dashboard -. Open the *Dashboard* page in {kib}. +. Open the *Dashboards* page in {kib}. . Locate the dashboard you want to edit. + @@ -67,7 +72,9 @@ TIP: When looking for a specific dashboard, you can filter them by tag or by cre . Click the dashboard *Title* you want to open. . Make sure that you are in **Edit** mode to be able to make changes to the dashboard. You can switch between **Edit** and **View** modes from the toolbar. - ++ +[role="screenshot"] +image::https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/bltf75cdb828cef8b5a/66f5cfcfad4f59f38b73ba64/switch-to-view-mode-8.15.0.gif[Switch between Edit and View modes, width 30%] . Make the changes that you need to the dashboard: ** Adjust the dashboard's settings @@ -86,7 +93,11 @@ NOTE: Once changes are saved, you can no longer revert them in one click, and in . In the toolbar, click *Reset*. -. On the *Reset dashboard* window, click *Reset dashboard*. +. On the *Reset dashboard?* window, click *Reset dashboard*. + ++ +[role="screenshot"] +image::https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/bltcd3dbda9caf48a9b/66f4957fc9f9c71ce7533774/reset-dashboard-8.15.0.gif[Reset dashboard to revert unsaved changes, width 30%] include::dashboard-controls.asciidoc[leveloffset=-1] @@ -108,7 +119,7 @@ In the toolbar, click *Edit*, then use the following options: * To resize, click the resize control, then drag to the new dimensions. -* To maximize to full screen, open the panel menu, then click *More > Maximize panel*. +* To maximize to full screen, open the panel menu, then click *More > Maximize*. + TIP: If you <> a dashboard while viewing a full screen panel, the generated link will directly open the same panel in full screen mode. @@ -126,7 +137,10 @@ Duplicated panels appear next to the original panel, and move the other panels t . In the toolbar, click *Edit*. -. Open the panel menu, then select *Duplicate panel*. +. Open the panel menu, then select *Duplicate*. ++ +[role="screenshot"] +image::images/duplicate-panels-8.15.0.png[Duplicate a panel, width=50%] [float] [[copy-to-dashboard]] @@ -137,6 +151,9 @@ Copy panels from one dashboard to another dashboard. . Open the panel menu, then select *More > Copy to dashboard*. . On the *Copy to dashboard* window, select the dashboard, then click *Copy and go to dashboard*. ++ +[role="screenshot"] +image:https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt64206db263cf5514/66f49286833cffb09bebd18d/copy-to-dashboard-8.15.0.gif[Copy a panel to another dashboard, width 30%] == Import dashboards diff --git a/docs/user/dashboard/dashboard-controls.asciidoc b/docs/user/dashboard/dashboard-controls.asciidoc index 05819fe567e30..1db623f7cea96 100644 --- a/docs/user/dashboard/dashboard-controls.asciidoc +++ b/docs/user/dashboard/dashboard-controls.asciidoc @@ -33,9 +33,12 @@ To add interactive Options list and Range slider controls, create the controls, . Open or create a new dashboard. -. Make sure you are in *Edit* mode, and select *Controls* > *Add control* in the dashboard toolbar. +. In *Edit* mode, select *Controls* > *Add control* in the dashboard toolbar. ++ +[role="screenshot"] +image::images/dashboard-add-control-8.15.0.png[Controls button in the toolbar with the Add Control option selected, width=60%] -. From the *Data view* dropdown, select the data view that contains the field you want to use for the *Control*. +. On the *Create control* flyout, from the *Data view* dropdown, select the data view that contains the field you want to use for the *Control*. . In the *Field* list, select the field you want to filter on. @@ -80,7 +83,10 @@ You can add one interactive time slider control to a dashboard. . Open or create a new dashboard. -. In the dashboard toolbar, click *Controls* > *Add time slider control*. +. In *Edit* mode, select *Controls* > *Add time slider control*. ++ +[role="screenshot"] +image::images/dashboard-add-time-slider-control-8.15.0.png[Controls button in the toolbar with the Add a time slider option selected, width=60%] . The time slider control uses the time range from the global time filter. To change the time range in the time slider control, <>. @@ -92,11 +98,14 @@ You can add one interactive time slider control to a dashboard. [[configure-controls-settings]] ==== Configure the controls settings -Several settings that apply to all controls of a same dashboard are available. +Several settings that apply to all controls of the same dashboard are available. -. In the dashboard toolbar, click *Controls*, then select *Settings*. +. In *Edit* mode, select *Controls* > *Settings*. ++ +[role="screenshot"] +image::images/dashboard-controls-settings-8.15.0.png[Controls button in the toolbar with the Settings option selected, width=60%] -. On the *Control settings* flyout, configure the settings: +. On the *Control settings* flyout, configure the following settings: * *Label position* — Specify where the control label appears. @@ -105,7 +114,7 @@ Several settings that apply to all controls of a same dashboard are available. ** **Apply global filters to controls** — Define whether controls should ignore or apply any filter specified in the main filter bar of the dashboard. ** **Apply global time range to controls** — Define whether controls should ignore or apply the main time range specified for the dashboard. Note that <> rely on the global time range and don't work properly when this option is disabled. -* *Selection* settings: +* *Selections* settings: ** *Validate user selections* — When selected, any selected option that results in no data is ignored. ** *Chain controls* — When selected, controls are applied sequentially from left to right, and line by line. Any selected options in one control narrows the available options in the next control. diff --git a/docs/user/dashboard/images/add_content_to_dashboard_8.15.0.png b/docs/user/dashboard/images/add_content_to_dashboard_8.15.0.png new file mode 100644 index 0000000000000..8a8b9b9d5d03b Binary files /dev/null and b/docs/user/dashboard/images/add_content_to_dashboard_8.15.0.png differ diff --git a/docs/user/dashboard/images/dashboard-add-control-8.15.0.png b/docs/user/dashboard/images/dashboard-add-control-8.15.0.png new file mode 100644 index 0000000000000..d4a21b32c4344 Binary files /dev/null and b/docs/user/dashboard/images/dashboard-add-control-8.15.0.png differ diff --git a/docs/user/dashboard/images/dashboard-add-time-slider-control-8.15.0.png b/docs/user/dashboard/images/dashboard-add-time-slider-control-8.15.0.png new file mode 100644 index 0000000000000..45c954e0c4b9b Binary files /dev/null and b/docs/user/dashboard/images/dashboard-add-time-slider-control-8.15.0.png differ diff --git a/docs/user/dashboard/images/dashboard-controls-settings-8.15.0.png b/docs/user/dashboard/images/dashboard-controls-settings-8.15.0.png new file mode 100644 index 0000000000000..ea348115b25d5 Binary files /dev/null and b/docs/user/dashboard/images/dashboard-controls-settings-8.15.0.png differ diff --git a/docs/user/dashboard/images/duplicate-panels-8.15.0.png b/docs/user/dashboard/images/duplicate-panels-8.15.0.png new file mode 100644 index 0000000000000..23c5b831dcdb4 Binary files /dev/null and b/docs/user/dashboard/images/duplicate-panels-8.15.0.png differ diff --git a/oas_docs/examples/create_es_query_esql_rule_request.yaml b/oas_docs/examples/create_es_query_esql_rule_request.yaml index 874f5f96b1f8d..0646699e8a41a 100644 --- a/oas_docs/examples/create_es_query_esql_rule_request.yaml +++ b/oas_docs/examples/create_es_query_esql_rule_request.yaml @@ -1,4 +1,6 @@ -summary: Create an Elasticsearch query rule that uses Elasticsearch Query Language (ES|QL). +summary: Elasticsearch query rule (ES|QL) +description: > + Create an Elasticsearch query rule that uses Elasticsearch Query Language (ES|QL) to define its query and a server log connector to send notifications. value: name: my Elasticsearch query ESQL rule params: diff --git a/oas_docs/examples/create_es_query_esql_rule_response.yaml b/oas_docs/examples/create_es_query_esql_rule_response.yaml index 2a0e786d5f52b..fa4ebd0977674 100644 --- a/oas_docs/examples/create_es_query_esql_rule_response.yaml +++ b/oas_docs/examples/create_es_query_esql_rule_response.yaml @@ -1,4 +1,5 @@ -summary: The create rule API returns a JSON object that contains details about the rule. +summary: Elasticsearch query rule (ES|QL) +description: The response for successfully creating an Elasticsearch query rule that uses Elasticsearch Query Language (ES|QL). value: id: e0d62360-78e8-11ee-9177-f7d404c8c945 enabled: true diff --git a/oas_docs/examples/create_es_query_kql_rule_request.yaml b/oas_docs/examples/create_es_query_kql_rule_request.yaml index e505fd8964463..8b9af0129912e 100644 --- a/oas_docs/examples/create_es_query_kql_rule_request.yaml +++ b/oas_docs/examples/create_es_query_kql_rule_request.yaml @@ -1,4 +1,5 @@ -summary: Create an Elasticsearch query rule that uses Kibana query language (KQL). +summary: Elasticsearch query rule (KQL) +description: Create an Elasticsearch query rule that uses Kibana query language (KQL). value: consumer: alerts name: my Elasticsearch query KQL rule diff --git a/oas_docs/examples/create_es_query_kql_rule_response.yaml b/oas_docs/examples/create_es_query_kql_rule_response.yaml index 0a30c4e6dd41e..4ecaf503e082c 100644 --- a/oas_docs/examples/create_es_query_kql_rule_response.yaml +++ b/oas_docs/examples/create_es_query_kql_rule_response.yaml @@ -1,4 +1,5 @@ -summary: The create rule API returns a JSON object that contains details about the rule. +summary: Elasticsearch query rule (KQL) +description: The response for successfully creating an Elasticsearch query rule that uses Kibana query language (KQL). value: id: 7bd506d0-2284-11ee-8fad-6101956ced88 enabled: true diff --git a/oas_docs/examples/create_es_query_rule_request.yaml b/oas_docs/examples/create_es_query_rule_request.yaml index bff7a8f0bd8f6..d021245ed7dff 100644 --- a/oas_docs/examples/create_es_query_rule_request.yaml +++ b/oas_docs/examples/create_es_query_rule_request.yaml @@ -1,4 +1,6 @@ -summary: Create an Elasticsearch query rule that uses Elasticsearch query domain specific language (DSL) to define its query and a server log connector to send notifications. +summary: Elasticsearch query rule (DSL) +description: > + Create an Elasticsearch query rule that uses Elasticsearch query domain specific language (DSL) to define its query and a server log connector to send notifications. value: actions: - group: query matched diff --git a/oas_docs/examples/create_es_query_rule_response.yaml b/oas_docs/examples/create_es_query_rule_response.yaml index 9601843a42e3b..81a634959ca3c 100644 --- a/oas_docs/examples/create_es_query_rule_response.yaml +++ b/oas_docs/examples/create_es_query_rule_response.yaml @@ -1,4 +1,5 @@ -summary: The create rule API returns a JSON object that contains details about the rule. +summary: Elasticsearch query rule (DSL) +description: The response for successfully creating an Elasticsearch query rule that uses Elasticsearch query domain specific language (DSL). value: id: 58148c70-407f-11ee-850e-c71febc4ca7f enabled: true diff --git a/oas_docs/examples/create_index_threshold_rule_request.yaml b/oas_docs/examples/create_index_threshold_rule_request.yaml index 53859628f57a1..146b0cff7d32b 100644 --- a/oas_docs/examples/create_index_threshold_rule_request.yaml +++ b/oas_docs/examples/create_index_threshold_rule_request.yaml @@ -1,4 +1,6 @@ -summary: Create an index threshold rule. +summary: Index threshold rule +description: > + Create an index threshold rule that uses a server log connector to send notifications when the threshold is met. value: actions: - id: 48de3460-f401-11ed-9f8e-399c75a2deeb diff --git a/oas_docs/examples/create_index_threshold_rule_response.yaml b/oas_docs/examples/create_index_threshold_rule_response.yaml index da172f5df9ff7..00de25aa06763 100644 --- a/oas_docs/examples/create_index_threshold_rule_response.yaml +++ b/oas_docs/examples/create_index_threshold_rule_response.yaml @@ -1,4 +1,5 @@ -summary: The create rule API returns a JSON object that contains details about the rule. +summary: Index threshold rule +description: The response for successfully creating an index threshold rule. value: actions: - group: threshold met diff --git a/oas_docs/examples/create_tracking_containment_rule_request.yaml b/oas_docs/examples/create_tracking_containment_rule_request.yaml index 2e474b59bbb1b..3a20a0bbe804a 100644 --- a/oas_docs/examples/create_tracking_containment_rule_request.yaml +++ b/oas_docs/examples/create_tracking_containment_rule_request.yaml @@ -1,4 +1,6 @@ -summary: Create a tracking containment rule. +summary: Tracking containment rule +description: > + Create a tracking containment rule that checks when an entity is contained or no longer contained within a boundary. value: consumer: alerts name: my tracking rule diff --git a/oas_docs/examples/create_tracking_containment_rule_response.yaml b/oas_docs/examples/create_tracking_containment_rule_response.yaml index c7d927ab2f1e4..e68d2a484afc7 100644 --- a/oas_docs/examples/create_tracking_containment_rule_response.yaml +++ b/oas_docs/examples/create_tracking_containment_rule_response.yaml @@ -1,4 +1,5 @@ -summary: The create rule API returns a JSON object that contains details about the rule. +summary: Tracking containment rule +description: The response for successfully creating a tracking containment rule. value: id: b6883f9d-5f70-4758-a66e-369d7c26012f name: my tracking rule diff --git a/oas_docs/examples/find_rules_response.yaml b/oas_docs/examples/find_rules_response.yaml index b6af1580d8e40..81ba04239d22a 100644 --- a/oas_docs/examples/find_rules_response.yaml +++ b/oas_docs/examples/find_rules_response.yaml @@ -1,4 +1,5 @@ -summary: Retrieve information about a rule. +summary: Index threshold rule +description: A response that contains information about an index threshold rule. value: page: 1 total: 1 diff --git a/oas_docs/examples/find_rules_response_conditional_action.yaml b/oas_docs/examples/find_rules_response_conditional_action.yaml index 857153be8fa38..cdbab5693e781 100644 --- a/oas_docs/examples/find_rules_response_conditional_action.yaml +++ b/oas_docs/examples/find_rules_response_conditional_action.yaml @@ -1,4 +1,5 @@ -summary: Retrieve information about a rule that has conditional actions. +summary: Security rule +description: A response that contains information about a security rule that has conditional actions. value: page: 1 total: 1 diff --git a/oas_docs/examples/update_rule_request.yaml b/oas_docs/examples/update_rule_request.yaml index bc5411bdaf023..0dcb976af993b 100644 --- a/oas_docs/examples/update_rule_request.yaml +++ b/oas_docs/examples/update_rule_request.yaml @@ -1,4 +1,5 @@ -summary: Update an index threshold rule. +summary: Index threshold rule +description: Update an index threshold rule that uses a server log connector to send notifications when the threshold is met. value: actions: - frequency: diff --git a/oas_docs/examples/update_rule_response.yaml b/oas_docs/examples/update_rule_response.yaml index e9d103bbd5495..b2a95ab870003 100644 --- a/oas_docs/examples/update_rule_response.yaml +++ b/oas_docs/examples/update_rule_response.yaml @@ -1,4 +1,5 @@ -summary: The update rule API returns a JSON object that contains details about the rule. +summary: Index threshold rule +description: The response for successfully updating an index threshold rule. value: id: ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74 consumer: alerts diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx index a607d69fb0633..a574d8061d3e2 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx @@ -17,6 +17,8 @@ interface Props { isSideNavCollapsed$: Observable; } +const PANEL_WIDTH = 290; + export const ProjectNavigation: FC> = ({ children, isSideNavCollapsed$, @@ -29,11 +31,10 @@ export const ProjectNavigation: FC> = ({ data-test-subj="projectLayoutSideNav" isCollapsed={isCollapsed} onCollapseToggle={toggleSideNav} - css={ - isCollapsed - ? undefined - : { overflow: 'visible', clipPath: 'polygon(0 0, 300% 0, 300% 100%, 0 100%)' } - } + css={{ + overflow: 'visible', + clipPath: `polygon(0 0, calc(var(--euiCollapsibleNavOffset) + ${PANEL_WIDTH}px) 0, calc(var(--euiCollapsibleNavOffset) + ${PANEL_WIDTH}px) 100%, 0 100%)`, + }} > {children} diff --git a/packages/kbn-esql-editor/src/history_local_storage.test.ts b/packages/kbn-esql-editor/src/history_local_storage.test.ts index 5e004affd2cec..c149dada84894 100644 --- a/packages/kbn-esql-editor/src/history_local_storage.test.ts +++ b/packages/kbn-esql-editor/src/history_local_storage.test.ts @@ -46,6 +46,24 @@ describe('history local storage', function () { ); }); + it('should update queries to cache correctly if they are the same with different format', function () { + addQueriesToCache({ + queryString: 'from kibana_sample_data_flights | limit 10 | stats meow = avg(woof) ', + timeZone: 'Browser', + status: 'success', + }); + + const historyItems = getCachedQueries(); + expect(historyItems.length).toBe(2); + expect(historyItems[1].timeRan).toBeDefined(); + expect(historyItems[1].status).toBe('success'); + + expect(mockSetItem).toHaveBeenCalledWith( + 'QUERY_HISTORY_ITEM_KEY', + JSON.stringify(historyItems) + ); + }); + it('should allow maximum x queries ', function () { addQueriesToCache( { diff --git a/packages/kbn-esql-editor/src/history_local_storage.ts b/packages/kbn-esql-editor/src/history_local_storage.ts index 259fb20db1465..c79561d5d3875 100644 --- a/packages/kbn-esql-editor/src/history_local_storage.ts +++ b/packages/kbn-esql-editor/src/history_local_storage.ts @@ -27,7 +27,7 @@ export interface QueryHistoryItem { const MAX_QUERIES_NUMBER = 20; const getKey = (queryString: string) => { - return queryString.replaceAll('\n', '').trim(); + return queryString.replaceAll('\n', '').trim().replace(/\s\s+/g, ' '); }; const getMomentTimeZone = (timeZone?: string) => { @@ -60,6 +60,9 @@ export const addQueriesToCache = ( item: QueryHistoryItem, maxQueriesAllowed = MAX_QUERIES_NUMBER ) => { + // if the user is working on multiple tabs + // the cachedQueries Map might not contain all + // the localStorage queries const queries = getHistoryItems('desc'); queries.forEach((queryItem) => { const trimmedQueryString = getKey(queryItem.queryString); @@ -77,15 +80,7 @@ export const addQueriesToCache = ( }); } - const queriesToStore = getCachedQueries(); - const localStorageQueries = getHistoryItems('desc'); - // if the user is working on multiple tabs - // the cachedQueries Map might not contain all - // the localStorage queries - const newQueries = localStorageQueries.filter( - (ls) => !queriesToStore.find((cachedQuery) => cachedQuery.queryString === ls.queryString) - ); - let allQueries = [...queriesToStore, ...newQueries]; + let allQueries = [...getCachedQueries()]; if (allQueries.length >= maxQueriesAllowed + 1) { const sortedByDate = allQueries.sort((a, b) => diff --git a/packages/kbn-unified-field-list/src/components/field_stats/field_top_values_bucket.tsx b/packages/kbn-unified-field-list/src/components/field_stats/field_top_values_bucket.tsx index dcc647a21e113..77266edc2de07 100755 --- a/packages/kbn-unified-field-list/src/components/field_stats/field_top_values_bucket.tsx +++ b/packages/kbn-unified-field-list/src/components/field_stats/field_top_values_bucket.tsx @@ -70,6 +70,18 @@ const FieldTopValuesBucket: React.FC = ({ } = { ...fieldTopValuesBucketOverridableProps, ...overrides }; const fieldLabel = (field?.subType as IFieldSubTypeMulti)?.multi?.parent ?? field.name; + const filterForLabel = i18n.translate('unifiedFieldList.fieldStats.filterValueButtonAriaLabel', { + defaultMessage: 'Filter for {field}: "{value}"', + values: { value: formattedFieldValue, field: fieldLabel }, + }); + const filterOutLabel = i18n.translate( + 'unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel', + { + defaultMessage: 'Filter out {field}: "{value}"', + values: { value: formattedFieldValue, field: fieldLabel }, + } + ); + return ( = ({ /> ) : (
- onAddFilter(field, fieldValue, '+')} - aria-label={i18n.translate( - 'unifiedFieldList.fieldStats.filterValueButtonAriaLabel', - { - defaultMessage: 'Filter for {field}: "{value}"', - values: { value: formattedFieldValue, field: fieldLabel }, - } - )} - data-test-subj={`plus-${fieldLabel}-${fieldValue}`} - style={{ - minHeight: 'auto', - minWidth: 'auto', - paddingRight: 2, - paddingLeft: 2, - paddingTop: 0, - paddingBottom: 0, - }} - /> - onAddFilter(field, fieldValue, '-')} - aria-label={i18n.translate( - 'unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel', - { - defaultMessage: 'Filter out {field}: "{value}"', - values: { value: formattedFieldValue, field: fieldLabel }, - } - )} - data-test-subj={`minus-${fieldLabel}-${fieldValue}`} - style={{ - minHeight: 'auto', - minWidth: 'auto', - paddingTop: 0, - paddingBottom: 0, - paddingRight: 2, - paddingLeft: 2, - }} - /> + + onAddFilter(field, fieldValue, '+')} + aria-label={filterForLabel} + data-test-subj={`plus-${fieldLabel}-${fieldValue}`} + style={{ + minHeight: 'auto', + minWidth: 'auto', + paddingRight: 2, + paddingLeft: 2, + paddingTop: 0, + paddingBottom: 0, + }} + /> + + + onAddFilter(field, fieldValue, '-')} + aria-label={filterOutLabel} + data-test-subj={`minus-${fieldLabel}-${fieldValue}`} + style={{ + minHeight: 'auto', + minWidth: 'auto', + paddingTop: 0, + paddingBottom: 0, + paddingRight: 2, + paddingLeft: 2, + }} + /> +
)} diff --git a/packages/kbn-unified-field-list/src/containers/unified_field_list_item/field_list_item.tsx b/packages/kbn-unified-field-list/src/containers/unified_field_list_item/field_list_item.tsx index c6f301bbdf69f..658bf96dc76c9 100644 --- a/packages/kbn-unified-field-list/src/containers/unified_field_list_item/field_list_item.tsx +++ b/packages/kbn-unified-field-list/src/containers/unified_field_list_item/field_list_item.tsx @@ -14,6 +14,7 @@ import { UiCounterMetricType } from '@kbn/analytics'; import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; import { Draggable } from '@kbn/dom-drag-drop'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { Filter } from '@kbn/es-query'; import type { SearchMode } from '../../types'; import { FieldItemButton, type FieldItemButtonProps } from '../../components/field_item_button'; import { @@ -200,6 +201,10 @@ export interface UnifiedFieldListItemProps { * Item size */ size: FieldItemButtonProps['size']; + /** + * Custom filters to apply for the field list, ex: namespace custom filter + */ + additionalFilters?: Filter[]; } function UnifiedFieldListItemComponent({ @@ -223,6 +228,7 @@ function UnifiedFieldListItemComponent({ groupIndex, itemIndex, size, + additionalFilters, }: UnifiedFieldListItemProps) { const [infoIsOpen, setOpen] = useState(false); @@ -288,6 +294,7 @@ function UnifiedFieldListItemComponent({ multiFields={multiFields} dataView={dataView} onAddFilter={addFilterAndClosePopover} + additionalFilters={additionalFilters} /> {searchMode === 'documents' && multiFields && ( diff --git a/packages/kbn-unified-field-list/src/containers/unified_field_list_item/field_list_item_stats.tsx b/packages/kbn-unified-field-list/src/containers/unified_field_list_item/field_list_item_stats.tsx index c83a0694c7b67..223a5e15ca6e7 100644 --- a/packages/kbn-unified-field-list/src/containers/unified_field_list_item/field_list_item_stats.tsx +++ b/packages/kbn-unified-field-list/src/containers/unified_field_list_item/field_list_item_stats.tsx @@ -27,10 +27,11 @@ export interface UnifiedFieldListItemStatsProps { dataView: DataView; multiFields?: Array<{ field: DataViewField; isSelected: boolean }>; onAddFilter: FieldStatsProps['onAddFilter']; + additionalFilters?: FieldStatsProps['filters']; } export const UnifiedFieldListItemStats: React.FC = React.memo( - ({ stateService, services, field, dataView, multiFields, onAddFilter }) => { + ({ stateService, services, field, dataView, multiFields, onAddFilter, additionalFilters }) => { const querySubscriberResult = useQuerySubscriber({ data: services.data, timeRangeUpdatesType: stateService.creationOptions.timeRangeUpdatesType, @@ -55,6 +56,11 @@ export const UnifiedFieldListItemStats: React.FC [services] ); + const filters = useMemo( + () => [...(querySubscriberResult.filters ?? []), ...(additionalFilters ?? [])], + [querySubscriberResult.filters, additionalFilters] + ); + if (!hasQuerySubscriberData(querySubscriberResult)) { return null; } @@ -63,7 +69,7 @@ export const UnifiedFieldListItemStats: React.FC & { /** * All fields: fields from data view and unmapped fields or columns from text-based search @@ -168,6 +169,7 @@ export const UnifiedFieldListSidebarComponent: React.FC { const { dataViews, core } = services; const useNewFieldsApi = useMemo( @@ -285,6 +287,7 @@ export const UnifiedFieldListSidebarComponent: React.FC ), @@ -304,6 +307,7 @@ export const UnifiedFieldListSidebarComponent: React.FC( createStateService({ options: getCreationOptions() }) @@ -151,11 +152,16 @@ const UnifiedFieldListSidebarContainer = memo( const searchMode: SearchMode | undefined = querySubscriberResult.searchMode; const isAffectedByGlobalFilter = Boolean(querySubscriberResult.filters?.length); + const filters = useMemo( + () => [...(querySubscriberResult.filters ?? []), ...(additionalFilters ?? [])], + [querySubscriberResult.filters, additionalFilters] + ); + const { isProcessing, refetchFieldsExistenceInfo } = useExistingFieldsFetcher({ disableAutoFetching: stateService.creationOptions.disableFieldsExistenceAutoFetching, dataViews: searchMode === 'documents' && dataView ? [dataView] : [], query: querySubscriberResult.query, - filters: querySubscriberResult.filters, + filters, fromDate: querySubscriberResult.fromDate, toDate: querySubscriberResult.toDate, services, diff --git a/src/plugins/data/common/search/aggs/metrics/lib/get_response_agg_config_class.test.ts b/src/plugins/data/common/search/aggs/metrics/lib/get_response_agg_config_class.test.ts new file mode 100644 index 0000000000000..71787269c9bfd --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/lib/get_response_agg_config_class.test.ts @@ -0,0 +1,34 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { getResponseAggId } from './get_response_agg_config_class'; + +describe('getResponseAggConfigClass', () => { + describe('getResponseAggId', () => { + it('should generate a dot-separated ID from parent/key', () => { + const id = getResponseAggId('parent', 'child'); + expect(id).toBe('parent.child'); + }); + + it('should use brackets/quotes if the value includes a dot', () => { + const id = getResponseAggId('parent', 'foo.bar'); + expect(id).toBe(`parent['foo.bar']`); + }); + + it('should escape quotes', () => { + const id = getResponseAggId('parent', `foo.b'ar`); + expect(id).toBe(`parent['foo.b\\'ar']`); + }); + + it('should escape backslashes', () => { + const id = getResponseAggId('parent', `f\\oo.b'ar`); + expect(id).toBe(`parent['f\\\\oo.b\\'ar']`); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/lib/get_response_agg_config_class.ts b/src/plugins/data/common/search/aggs/metrics/lib/get_response_agg_config_class.ts index 1a99488ce8a4e..65263e2bc4a5f 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/get_response_agg_config_class.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/get_response_agg_config_class.ts @@ -31,6 +31,15 @@ export interface IResponseAggConfig extends IMetricAggConfig { parentId: IMetricAggConfig['id']; } +export function getResponseAggId(parentId: string, key: string) { + const subId = String(key); + if (subId.indexOf('.') > -1) { + return parentId + "['" + subId.replace(/[\\']/g, '\\$&') + "']"; // $& means the whole matched string + } else { + return parentId + '.' + subId; + } +} + export const create = (parentAgg: IMetricAggConfig, props: Partial) => { /** * AggConfig "wrapper" for multi-value metric aggs which @@ -41,17 +50,7 @@ export const create = (parentAgg: IMetricAggConfig, props: Partial -1) { - id = parentId + "['" + subId.replace(/'/g, "\\'") + "']"; - } else { - id = parentId + '.' + subId; - } - - this.id = id; + this.id = getResponseAggId(parentId, key); this.key = key; this.parentId = parentId; } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 601338333226f..49e645e3f2206 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -103,6 +103,8 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { state.dataView!, state.isDataViewLoading, ]); + const customFilters = useInternalStateSelector((state) => state.customFilters); + const dataState: DataMainMsg = useDataState(main$); const savedSearch = useSavedSearchInitial(); @@ -404,6 +406,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { onFieldEdited={onFieldEdited} onDataViewCreated={stateContainer.actions.onDataViewCreated} sidebarToggleState$={sidebarToggleState$} + additionalFilters={customFilters} /> } mainPanel={ diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index 59b008d99e494..80a3b9d412c76 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -23,6 +23,7 @@ import { FieldsGroupNames, } from '@kbn/unified-field-list'; import { calcFieldCounts } from '@kbn/discover-utils/src/utils/calc_field_counts'; +import { Filter } from '@kbn/es-query'; import { PLUGIN_ID } from '../../../../../common'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DataDocuments$ } from '../../state_management/discover_data_state_container'; @@ -127,6 +128,10 @@ export interface DiscoverSidebarResponsiveProps { fieldListVariant?: UnifiedFieldListSidebarContainerProps['variant']; sidebarToggleState$: BehaviorSubject; + /** + * Custom filters to apply for the field list, ex: namespace custom filter + */ + additionalFilters?: Filter[]; } /** @@ -153,6 +158,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) onAddField, onRemoveField, sidebarToggleState$, + additionalFilters, } = props; const [sidebarState, dispatchSidebarStateAction] = useReducer( discoverSidebarReducer, @@ -383,6 +389,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) onFieldEdited={onFieldEdited} prependInFlyout={prependDataViewPickerForMobile} additionalFieldGroups={additionalFieldGroups} + additionalFilters={additionalFilters} /> ) : null} diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap index a71babb2977d1..f39970e15f6fe 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap @@ -4,12 +4,20 @@ exports[`TableActions getFieldCellActions should render correctly for undefined Array [ , , getFieldCellActions({ rows, onFilter: filter, onToggleColumn }), - [rows, filter, onToggleColumn] + () => getFieldCellActions({ rows, isEsqlMode, onFilter: filter, onToggleColumn }), + [rows, isEsqlMode, filter, onToggleColumn] ); const fieldValueCellActions = useMemo( - () => getFieldValueCellActions({ rows, onFilter: filter }), - [rows, filter] + () => getFieldValueCellActions({ rows, isEsqlMode, onFilter: filter }), + [rows, isEsqlMode, filter] ); useWindowSize(); // trigger re-render on window resize to recalculate the grid container height diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx index 9dd4305cd4b00..c29c7d6a4452f 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx @@ -11,26 +11,27 @@ import React from 'react'; import { getFieldCellActions, getFieldValueCellActions } from './table_cell_actions'; import { FieldRow } from './field_row'; import { buildDataTableRecord } from '@kbn/discover-utils'; -import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { render, screen } from '@testing-library/react'; +import { dataViewMockWithTimeField } from '@kbn/discover-utils/src/__mocks__'; describe('TableActions', () => { - const rows: FieldRow[] = [ + const getRows = (fieldName = 'message', fieldValue: unknown = 'test'): FieldRow[] => [ new FieldRow({ - name: 'message', - flattenedValue: 'flattenedField', + name: fieldName, + flattenedValue: fieldValue, hit: buildDataTableRecord( { _ignored: [], _index: 'test', _id: '1', _source: { - message: 'test', + [fieldName]: fieldValue, }, }, - dataView + dataViewMockWithTimeField ), - dataView, + dataView: dataViewMockWithTimeField, fieldFormats: {} as FieldFormatsStart, isPinned: false, columnsMeta: undefined, @@ -49,23 +50,32 @@ describe('TableActions', () => { describe('getFieldCellActions', () => { it('should render correctly for undefined functions', () => { expect( - getFieldCellActions({ rows, onFilter: undefined, onToggleColumn: jest.fn() }).map((item) => - item(EuiCellParams) - ) + getFieldCellActions({ + rows: getRows(), + isEsqlMode: false, + onFilter: undefined, + onToggleColumn: jest.fn(), + }).map((item) => item(EuiCellParams)) ).toMatchSnapshot(); expect( - getFieldCellActions({ rows, onFilter: undefined, onToggleColumn: undefined }).map((item) => - item(EuiCellParams) - ) + getFieldCellActions({ + rows: getRows(), + isEsqlMode: false, + onFilter: undefined, + onToggleColumn: undefined, + }).map((item) => item(EuiCellParams)) ).toMatchSnapshot(); }); it('should render the panels correctly for defined onFilter function', () => { expect( - getFieldCellActions({ rows, onFilter: jest.fn(), onToggleColumn: jest.fn() }).map((item) => - item(EuiCellParams) - ) + getFieldCellActions({ + rows: getRows(), + isEsqlMode: false, + onFilter: jest.fn(), + onToggleColumn: jest.fn(), + }).map((item) => item(EuiCellParams)) ).toMatchSnapshot(); }); }); @@ -73,14 +83,72 @@ describe('TableActions', () => { describe('getFieldValueCellActions', () => { it('should render correctly for undefined functions', () => { expect( - getFieldValueCellActions({ rows, onFilter: undefined }).map((item) => item(EuiCellParams)) + getFieldValueCellActions({ rows: getRows(), isEsqlMode: false, onFilter: undefined }).map( + (item) => item(EuiCellParams) + ) ).toMatchSnapshot(); }); it('should render the panels correctly for defined onFilter function', () => { expect( - getFieldValueCellActions({ rows, onFilter: jest.fn() }).map((item) => item(EuiCellParams)) + getFieldValueCellActions({ rows: getRows(), isEsqlMode: false, onFilter: jest.fn() }).map( + (item) => item(EuiCellParams) + ) ).toMatchSnapshot(); }); + + it('should allow filtering in ES|QL mode', () => { + const actions = getFieldValueCellActions({ + rows: getRows('extension'), + isEsqlMode: true, + onFilter: jest.fn(), + }).map((Action, i) => ( + ( +
{JSON.stringify(props)}
+ )} + /> + )); + render(<>{actions}); + const filterForProps = JSON.parse( + screen.getByTestId('addFilterForValueButton-extension').innerHTML + ); + expect(filterForProps.disabled).toBe(false); + expect(filterForProps.title).toBe('Filter for value'); + const filterOutProps = JSON.parse( + screen.getByTestId('addFilterOutValueButton-extension').innerHTML + ); + expect(filterOutProps.disabled).toBe(false); + expect(filterOutProps.title).toBe('Filter out value'); + }); + + it('should not allow filtering in ES|QL mode for multivalue fields', () => { + const actions = getFieldValueCellActions({ + rows: getRows('extension', ['foo', 'bar']), + isEsqlMode: true, + onFilter: jest.fn(), + }).map((Action, i) => ( + ( +
{JSON.stringify(props)}
+ )} + /> + )); + render(<>{actions}); + const filterForProps = JSON.parse( + screen.getByTestId('addFilterForValueButton-extension').innerHTML + ); + expect(filterForProps.disabled).toBe(true); + expect(filterForProps.title).toBe('Multivalue filtering is not supported in ES|QL'); + const filterOutProps = JSON.parse( + screen.getByTestId('addFilterOutValueButton-extension').innerHTML + ); + expect(filterOutProps.disabled).toBe(true); + expect(filterOutProps.title).toBe('Multivalue filtering is not supported in ES|QL'); + }); }); }); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx index e9c5a70770ca5..8bbb0f31d4798 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx @@ -16,9 +16,10 @@ import { FieldRow } from './field_row'; interface TableActionsProps { Component: EuiDataGridColumnCellActionProps['Component']; row: FieldRow | undefined; // as we pass `rows[rowIndex]` it's safer to assume that `row` prop can be undefined + isEsqlMode: boolean | undefined; } -export function isFilterInOutPairDisabled( +function isFilterInOutPairDisabled( row: FieldRow | undefined, onFilter: DocViewFilterFn | undefined ): boolean { @@ -58,9 +59,17 @@ export function getFilterInOutPairDisabledWarning( : undefined; } -export const FilterIn: React.FC = ({ +const esqlMultivalueFilteringDisabled = i18n.translate( + 'unifiedDocViewer.docViews.table.esqlMultivalueFilteringDisabled', + { + defaultMessage: 'Multivalue filtering is not supported in ES|QL', + } +); + +const FilterIn: React.FC = ({ Component, row, + isEsqlMode, onFilter, }) => { if (!row) { @@ -81,12 +90,14 @@ export const FilterIn: React.FC onFilter(dataViewField, flattenedValue, '+')} > @@ -95,9 +106,10 @@ export const FilterIn: React.FC = ({ +const FilterOut: React.FC = ({ Component, row, + isEsqlMode, onFilter, }) => { if (!row) { @@ -118,12 +130,14 @@ export const FilterOut: React.FC onFilter(dataViewField, flattenedValue, '-')} > @@ -132,7 +146,7 @@ export const FilterOut: React.FC = ({ Component, row, onFilter }) => { +const FilterExist: React.FC = ({ + Component, + row, + onFilter, +}) => { if (!row) { return null; } @@ -198,7 +214,7 @@ export const FilterExist: React.FC< ); }; -export const ToggleColumn: React.FC< +const ToggleColumn: React.FC< TableActionsProps & { onToggleColumn: ((field: string) => void) | undefined; } @@ -236,10 +252,12 @@ export const ToggleColumn: React.FC< export function getFieldCellActions({ rows, + isEsqlMode, onFilter, onToggleColumn, }: { rows: FieldRow[]; + isEsqlMode: boolean | undefined; onFilter?: DocViewFilterFn; onToggleColumn: ((field: string) => void) | undefined; }) { @@ -247,7 +265,14 @@ export function getFieldCellActions({ ...(onFilter ? [ ({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => { - return ; + return ( + + ); }, ] : []), @@ -258,6 +283,7 @@ export function getFieldCellActions({ ); @@ -269,18 +295,34 @@ export function getFieldCellActions({ export function getFieldValueCellActions({ rows, + isEsqlMode, onFilter, }: { rows: FieldRow[]; + isEsqlMode: boolean | undefined; onFilter?: DocViewFilterFn; }) { return onFilter ? [ ({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => { - return ; + return ( + + ); }, ({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => { - return ; + return ( + + ); }, ] : []; diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/params_es_query_esql_rule.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/params_es_query_esql_rule.yaml index 342231145066b..ad79337189c09 100644 --- a/x-pack/plugins/alerting/docs/openapi/components/schemas/params_es_query_esql_rule.yaml +++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/params_es_query_esql_rule.yaml @@ -2,10 +2,7 @@ title: Elasticsearch ES|QL query rule params description: > An Elasticsearch query rule can run an ES|QL query and compare the number of matches to a configured threshold. These parameters are appropriate when `rule_type_id` is `.es-query`. - NOTE: This functionality is in technical pre view and may be changed or removed in a future release. - Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. type: object -x-technical-preview: true required: - esqlQuery - searchType diff --git a/x-pack/plugins/file_upload/server/import_data.ts b/x-pack/plugins/file_upload/server/import_data.ts index afcec55107ff9..7f69a2be14404 100644 --- a/x-pack/plugins/file_upload/server/import_data.ts +++ b/x-pack/plugins/file_upload/server/import_data.ts @@ -49,6 +49,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) { createdPipelineId = pipelineId; } else { createdIndex = index; + createdPipelineId = pipelineId; } let failures: ImportFailure[] = []; diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.test.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.test.ts index 8e064a92a96a1..e5071b5272c27 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.test.ts @@ -7,7 +7,9 @@ import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; -import { getAvailableVersionsHandler } from './handlers'; +import { getAgentStatusForAgentPolicy } from '../../services/agents/status'; + +import { getAgentStatusForAgentPolicyHandler, getAvailableVersionsHandler } from './handlers'; jest.mock('../../services/agents/versions', () => { return { @@ -24,16 +26,60 @@ jest.mock('../../services/app_context', () => { }; }); -describe('getAvailableVersionsHandler', () => { - it('should return the value from getAvailableVersions', async () => { - const ctx = coreMock.createCustomRequestHandlerContext(coreMock.createRequestHandlerContext()); - const response = httpServerMock.createResponseFactory(); +jest.mock('../../services/agents/status', () => ({ + getAgentStatusForAgentPolicy: jest.fn(), +})); + +describe('Handlers', () => { + describe('getAgentStatusForAgentPolicyHandler', () => { + it.each([ + { requested: 'policy-id-1', called: ['policy-id-1'] }, + { requested: ['policy-id-2'], called: ['policy-id-2'] }, + { requested: ['policy-id-3', 'policy-id-4'], called: ['policy-id-3', 'policy-id-4'] }, + ...[undefined, '', []].map((requested) => ({ requested, called: undefined })), + ])('calls getAgentStatusForAgentPolicy with correct parameters', async (item) => { + const request = { + query: { + policyId: 'policy-id', + kuery: 'kuery', + policyIds: item.requested, + }, + }; + const response = httpServerMock.createResponseFactory(); + + await getAgentStatusForAgentPolicyHandler( + { + core: coreMock.createRequestHandlerContext(), + fleet: { internalSoClient: {} }, + } as any, + request as any, + response + ); + + expect(getAgentStatusForAgentPolicy).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + 'policy-id', + 'kuery', + undefined, + item.called + ); + }); + }); + + describe('getAvailableVersionsHandler', () => { + it('should return the value from getAvailableVersions', async () => { + const ctx = coreMock.createCustomRequestHandlerContext( + coreMock.createRequestHandlerContext() + ); + const response = httpServerMock.createResponseFactory(); - await getAvailableVersionsHandler(ctx, httpServerMock.createKibanaRequest(), response); + await getAvailableVersionsHandler(ctx, httpServerMock.createKibanaRequest(), response); - expect(response.ok).toBeCalled(); - expect(response.ok.mock.calls[0][0]?.body).toEqual({ - items: ['8.1.0', '8.0.0', '7.17.0'], + expect(response.ok).toBeCalled(); + expect(response.ok.mock.calls[0][0]?.body).toEqual({ + items: ['8.1.0', '8.0.0', '7.17.0'], + }); }); }); }); diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 67703bba5caae..4f4b5592f2d04 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -323,13 +323,22 @@ export const getAgentStatusForAgentPolicyHandler: FleetRequestHandler< const [coreContext, fleetContext] = await Promise.all([context.core, context.fleet]); const esClient = coreContext.elasticsearch.client.asInternalUser; const soClient = fleetContext.internalSoClient; + + const parsePolicyIds = (policyIds: string | string[] | undefined): string[] | undefined => { + if (!policyIds || !policyIds.length) { + return undefined; + } + + return Array.isArray(policyIds) ? policyIds : [policyIds]; + }; + const results = await getAgentStatusForAgentPolicy( esClient, soClient, request.query.policyId, request.query.kuery, coreContext.savedObjects.client.getCurrentNamespace(), - request.query.policyIds + parsePolicyIds(request.query.policyIds) ); const body: GetAgentStatusResponse = { results }; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 82cae68602e94..8f44d5554501e 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -241,7 +241,7 @@ export const PostBulkUpdateAgentTagsRequestSchema = { export const GetAgentStatusRequestSchema = { query: schema.object({ policyId: schema.maybe(schema.string()), - policyIds: schema.maybe(schema.arrayOf(schema.string())), + policyIds: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), kuery: schema.maybe( schema.string({ validate: (value: string) => { diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/assets/auto_detect.sh b/x-pack/plugins/observability_solution/observability_onboarding/public/assets/auto_detect.sh index 30d093cf515df..ebdcdeb0d81dc 100755 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/assets/auto_detect.sh +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/assets/auto_detect.sh @@ -289,16 +289,16 @@ apply_elastic_agent_config() { local decoded_ingest_api_key=$(echo "$ingest_api_key_encoded" | base64 -d) # Verify that the downloaded archive contains the expected `elastic-agent.yml` file - tar --list --file "$elastic_agent_tmp_config_path" --include 'elastic-agent.yml' >/dev/null && + tar --list --file "$elastic_agent_tmp_config_path" | grep "$(basename "$elastic_agent_config_path")" >/dev/null && # Remove existing config file including `inputs.d` directory rm -rf "$elastic_agent_config_path" "$(dirname "$elastic_agent_config_path")/inputs.d" && # Extract new config files from downloaded archive - tar --extract --file "$elastic_agent_tmp_config_path" --include 'elastic-agent.yml' --include 'inputs.d/*.yml' --directory "$(dirname "$elastic_agent_config_path")" && + tar --extract --file "$elastic_agent_tmp_config_path" --directory "$(dirname "$elastic_agent_config_path")" && # Replace placeholder with the Ingest API key - sed -i '' "s/\${API_KEY}/$decoded_ingest_api_key/" "$elastic_agent_config_path" + sed -i='' "s/\${API_KEY}/$decoded_ingest_api_key/" "$elastic_agent_config_path" if [ "$?" -eq 0 ]; then printf "\e[1;32m✓\e[0m %s\n" "Config written to:" - tar --list --file "$elastic_agent_tmp_config_path" --include 'elastic-agent.yml' --include 'inputs.d/*.yml' | while read -r file; do + tar --list --file "$elastic_agent_tmp_config_path" | grep '\.yml$' | while read -r file; do echo " - $(dirname "$elastic_agent_config_path")/$file" done diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/flow/route.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/flow/route.ts index 6b30ed8c3e089..94cf02fc4e067 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/flow/route.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/flow/route.ts @@ -263,9 +263,12 @@ const createFlowRoute = createObservabilityOnboardingServerRoute({ * * The request format is TSV (tab-separated values) to simplify parsing in bash. * - * The response format is either a YAML file or a tarball containing the Elastic Agent + * The response format is either a YAML file or a tar archive containing the Elastic Agent * configuration, depending on the `Accept` header. * + * Errors during installation are ignore unless all integrations fail to install. When that happens + * a 500 Internal Server Error is returned with the first error message. + * * Example request: * * ```text @@ -335,10 +338,21 @@ const integrationsInstallRoute = createObservabilityOnboardingServerRoute({ let installedIntegrations: InstalledIntegration[] = []; try { - installedIntegrations = await ensureInstalledIntegrations( + const settledResults = await ensureInstalledIntegrations( integrationsToInstall, packageClient ); + installedIntegrations = settledResults.reduce((acc, result) => { + if (result.status === 'fulfilled') { + acc.push(result.value); + } + return acc; + }, []); + // Errors during installation are ignore unless all integrations fail to install. When that happens + // a 500 Internal Server Error is returned with the first error message. + if (!installedIntegrations.length) { + throw (settledResults[0] as PromiseRejectedResult).reason; + } } catch (error) { if (error instanceof FleetUnauthorizedError) { return response.forbidden({ @@ -401,8 +415,8 @@ export type IntegrationToInstall = RegistryIntegrationToInstall | CustomIntegrat async function ensureInstalledIntegrations( integrationsToInstall: IntegrationToInstall[], packageClient: PackageClient -): Promise { - return Promise.all( +): Promise>> { + return Promise.allSettled( integrationsToInstall.map(async (integration) => { const { pkgName, installSource } = integration; diff --git a/x-pack/plugins/security_solution/public/common/components/split_accordion/index.ts b/x-pack/plugins/security_solution/public/common/components/split_accordion/index.ts new file mode 100644 index 0000000000000..6b05d7594e20a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/split_accordion/index.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './split_accordion'; diff --git a/x-pack/plugins/security_solution/public/common/components/split_accordion/split_accordion.tsx b/x-pack/plugins/security_solution/public/common/components/split_accordion/split_accordion.tsx new file mode 100644 index 0000000000000..668473919b97b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/split_accordion/split_accordion.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiAccordion, EuiSplitPanel, useEuiTheme, useGeneratedHtmlId } from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { PropsWithChildren } from 'react'; + +interface SplitAccordionProps { + header: React.ReactNode; + initialIsOpen?: boolean; + 'data-test-subj'?: string; +} + +export const SplitAccordion = ({ + header, + initialIsOpen, + 'data-test-subj': dataTestSubj, + children, +}: PropsWithChildren) => { + const accordionId = useGeneratedHtmlId(); + const { euiTheme } = useEuiTheme(); + + return ( + + + + {children} + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx index 86c85b7b7a89b..7051c7c496e45 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx @@ -8,8 +8,8 @@ import { EuiFlexGroup, EuiHorizontalRule, EuiTitle } from '@elastic/eui'; import { camelCase, startCase } from 'lodash'; import React from 'react'; +import { SplitAccordion } from '../../../../../common/components/split_accordion'; import { DiffView } from '../json_diff/diff_view'; -import { RuleDiffPanelWrapper } from './panel_wrapper'; import type { FormattedFieldDiff, FieldDiff } from '../../../model/rule_details/rule_field_diff'; import { fieldToDisplayNameMap } from './translations'; @@ -46,14 +46,24 @@ export const FieldGroupDiffComponent = ({ fieldsGroupName, }: FieldDiffComponentProps) => { const { fieldDiffs, shouldShowSubtitles } = ruleDiffs; + return ( - + +
{fieldToDisplayNameMap[fieldsGroupName] ?? startCase(camelCase(fieldsGroupName))}
+ + } + initialIsOpen={true} + data-test-subj="ruleUpgradePerFieldDiff" + > {fieldDiffs.map(({ currentVersion, targetVersion, fieldName: specificFieldName }, index) => { - const shouldShowSeparator = index !== fieldDiffs.length - 1; + const isLast = index === fieldDiffs.length - 1; + return ( ); })} -
+ ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/index.ts index 6effbcf3af931..cef0491a53e22 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/index.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/index.ts @@ -7,5 +7,4 @@ export * from './field_diff'; export * from './header_bar'; -export * from './panel_wrapper'; export * from './rule_diff_section'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/panel_wrapper.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/panel_wrapper.tsx deleted file mode 100644 index f0c86a68cafaf..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/panel_wrapper.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiAccordion, EuiSplitPanel, EuiTitle, useEuiTheme } from '@elastic/eui'; -import { camelCase, startCase } from 'lodash'; -import { css } from '@emotion/css'; -import React from 'react'; -import { fieldToDisplayNameMap } from './translations'; - -interface RuleDiffPanelWrapperProps { - fieldName: string; - children: React.ReactNode; -} - -export const RuleDiffPanelWrapper = ({ fieldName, children }: RuleDiffPanelWrapperProps) => { - const { euiTheme } = useEuiTheme(); - - return ( - - -
{fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}
- - } - > - - {children} - -
-
- ); -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx index 3a6f3e366d848..9ef207b0bb998 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx @@ -6,7 +6,6 @@ */ import React, { useState } from 'react'; -import { EuiSpacer } from '@elastic/eui'; import { VersionsPicker } from '../versions_picker/versions_picker'; import type { Version } from '../versions_picker/constants'; import { SelectedVersions } from '../versions_picker/constants'; @@ -17,6 +16,7 @@ import type { } from '../../../../../../../common/api/detection_engine'; import { getSubfieldChanges } from './get_subfield_changes'; import { SubfieldChanges } from './subfield_changes'; +import { SideHeader } from '../components/side_header'; interface ComparisonSideProps { fieldName: FieldName; @@ -42,12 +42,13 @@ export function ComparisonSide({ return ( <> - - + + + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/utils.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/utils.ts index fd16366f1a76e..dd52da04982f3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/utils.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/utils.ts @@ -26,7 +26,7 @@ export function pickFieldValueForVersion { + fieldName: FieldName; + fieldThreeWayDiff: RuleFieldsDiff[FieldName]; + finalDiffableRule: DiffableRule; +} + +export function FieldUpgradeConflictsResolver({ + fieldName, + fieldThreeWayDiff, + finalDiffableRule, +}: FieldUpgradeConflictsResolverProps): JSX.Element { + const { euiTheme } = useEuiTheme(); + const hasConflict = fieldThreeWayDiff.conflict !== ThreeWayDiffConflict.NONE; + + return ( + <> + } + initialIsOpen={hasConflict} + data-test-subj="ruleUpgradePerFieldDiff" + > + + + } + resolvedValue={finalDiffableRule[fieldName] as DiffableAllFields[FieldName]} + /> + + + + + + + + + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx new file mode 100644 index 0000000000000..2821a0a179b91 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx @@ -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 React from 'react'; +import { camelCase, startCase } from 'lodash'; +import { EuiTitle } from '@elastic/eui'; +import { fieldToDisplayNameMap } from '../../diff_components/translations'; + +interface FieldUpgradeConflictsResolverHeaderProps { + fieldName: string; +} + +export function FieldUpgradeConflictsResolverHeader({ + fieldName, +}: FieldUpgradeConflictsResolverHeaderProps): JSX.Element { + return ( + +
{fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}
+
+ ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx new file mode 100644 index 0000000000000..57af1b340c776 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { + RuleUpgradeState, + SetRuleFieldResolvedValueFn, +} from '../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state'; +import { FieldUpgradeConflictsResolver } from './field_upgrade_conflicts_resolver'; + +interface RuleUpgradeConflictsResolverProps { + ruleUpgradeState: RuleUpgradeState; + setRuleFieldResolvedValue: SetRuleFieldResolvedValueFn; +} + +export function RuleUpgradeConflictsResolver({ + ruleUpgradeState, + setRuleFieldResolvedValue, +}: RuleUpgradeConflictsResolverProps): JSX.Element { + const fieldDiffEntries = Object.entries(ruleUpgradeState.diff.fields) as Array< + [ + keyof typeof ruleUpgradeState.diff.fields, + Required[keyof typeof ruleUpgradeState.diff.fields] + ] + >; + const fields = fieldDiffEntries.map(([fieldName, fieldDiff]) => ( + + )); + + return <>{fields}; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx new file mode 100644 index 0000000000000..7ecde8059cc2f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { RuleUpgradeState } from '../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state'; +import { + UtilityBar, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../../../common/components/utility_bar'; +import * as i18n from './translations'; + +interface UpgradeInfoBarProps { + ruleUpgradeState: RuleUpgradeState; +} + +export function RuleUpgradeInfoBar({ ruleUpgradeState }: UpgradeInfoBarProps): JSX.Element { + const numOfFieldsWithUpdates = ruleUpgradeState.diff.num_fields_with_updates; + const numOfConflicts = ruleUpgradeState.diff.num_fields_with_conflicts; + + return ( + + + + + {i18n.NUM_OF_FIELDS_WITH_UPDATES(numOfFieldsWithUpdates)} + + + + + {i18n.NUM_OF_CONFLICTS(numOfConflicts)} + + + + + + + + + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/side_header.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/side_header.tsx new file mode 100644 index 0000000000000..574e3f526f856 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/side_header.tsx @@ -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 type { PropsWithChildren } from 'react'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; + +export function SideHeader({ children }: PropsWithChildren<{}>) { + const { euiTheme } = useEuiTheme(); + + return ( + <> + + {children} + + + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx new file mode 100644 index 0000000000000..620b3ac1c0ba8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; + +export const NUM_OF_FIELDS_WITH_UPDATES = (count: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.diffTab.fieldsWithUpdates', + { + values: { count }, + defaultMessage: 'Upgrade has {count} {count, plural, one {field} other {fields}}', + } + ); + +export const NUM_OF_CONFLICTS = (count: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.diffTab.numOfConflicts', + { + values: { count }, + defaultMessage: '{count} {count, plural, one {conflict} other {conflicts}}', + } + ); + +const UPGRADE_RULES_DOCS_LINK = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.updateYourRulesDocsLink', + { + defaultMessage: 'update your rules', + } +); + +export function RuleUpgradeHelper(): JSX.Element { + const { + docLinks: { + links: { + securitySolution: { manageDetectionRules }, + }, + }, + } = useKibana().services; + const manageDetectionRulesSnoozeSection = `${manageDetectionRules}#edit-rules-settings`; + + return ( + + {UPGRADE_RULES_DOCS_LINK} + + ), + }} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx new file mode 100644 index 0000000000000..0685d064b32d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiTitle } from '@elastic/eui'; +import type { DiffableRule } from '../../../../../../../common/api/detection_engine'; +import { FieldReadOnly } from '../final_readonly/field_readonly'; +import { SideHeader } from '../components/side_header'; +import { FinalSideHelpInfo } from './final_side_help_info'; +import * as i18n from './translations'; + +interface FinalSideProps { + fieldName: string; + finalDiffableRule: DiffableRule; +} + +export function FinalSide({ fieldName, finalDiffableRule }: FinalSideProps): JSX.Element { + return ( + <> + + +

+ {i18n.UPGRADED_VERSION} + +

+
+
+ + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side_help_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side_help_info.tsx new file mode 100644 index 0000000000000..766692e9efecd --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side_help_info.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useToggle } from 'react-use'; +import { EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +/** + * Theme doesn't expose width variables. Using provided size variables will require + * multiplying it by another magic constant. + * + * 320px width looks + * like a [commonly used width in EUI](https://github.com/search?q=repo%3Aelastic%2Feui%20320&type=code). + */ +const POPOVER_WIDTH = 320; + +export function FinalSideHelpInfo(): JSX.Element { + const [isPopoverOpen, togglePopover] = useToggle(false); + + const button = ( + + ); + + return ( + + + + + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts new file mode 100644 index 0000000000000..aa9b4885a964d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UPGRADED_VERSION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.upgradedVersion', + { + defaultMessage: 'Upgraded version', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx new file mode 100644 index 0000000000000..10823b8045c96 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx @@ -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. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import type { + RuleUpgradeState, + SetRuleFieldResolvedValueFn, +} from '../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state'; +import { RuleUpgradeInfoBar } from './components/rule_upgrade_info_bar'; +import { RuleUpgradeConflictsResolver } from './components/rule_upgrade_conflicts_resolver'; + +interface RuleUpgradeConflictsResolverTabProps { + ruleUpgradeState: RuleUpgradeState; + setRuleFieldResolvedValue: SetRuleFieldResolvedValueFn; +} + +export function RuleUpgradeConflictsResolverTab({ + ruleUpgradeState, + setRuleFieldResolvedValue, +}: RuleUpgradeConflictsResolverTabProps): JSX.Element { + return ( + <> + + + + + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx deleted file mode 100644 index 5117fa2d7b93b..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import type { DiffableRule } from '../../../../../common/api/detection_engine'; -import type { SetFieldResolvedValueFn } from '../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state'; - -interface ThreeWayDiffTabProps { - finalDiffableRule: DiffableRule; - setFieldResolvedValue: SetFieldResolvedValueFn; -} - -export function ThreeWayDiffTab({ - finalDiffableRule, - setFieldResolvedValue, -}: ThreeWayDiffTabProps): JSX.Element { - return <>{JSON.stringify(finalDiffableRule)}; -} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts index f0b96bad1aff5..89c22a285e327 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts @@ -28,13 +28,6 @@ export const UPDATES_TAB_LABEL = i18n.translate( } ); -export const DIFF_TAB_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetails.diffTabLabel', - { - defaultMessage: 'Diff', - } -); - export const JSON_VIEW_UPDATES_TAB_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.jsonViewUpdatesTabLabel', { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx index 4fee1cf8f3560..0ed1bdadb6316 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -9,7 +9,7 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -import { ThreeWayDiffTab } from '../../../../rule_management/components/rule_details/three_way_diff_tab'; +import { RuleUpgradeConflictsResolverTab } from '../../../../rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab'; import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab'; import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs'; @@ -137,7 +137,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ filterOptions, rules: ruleUpgradeInfos, }); - const { rulesUpgradeState, setFieldResolvedValue } = + const { rulesUpgradeState, setRuleFieldResolvedValue } = usePrebuiltRulesUpgradeState(filteredRuleUpgradeInfos); // Wrapper to add confirmation modal for users who may be running older ML Jobs that would @@ -222,7 +222,46 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ return []; } - const extraTabs = [ + const jsonViewUpdates = { + id: 'jsonViewUpdates', + name: ( + + <>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL} + + ), + content: ( + + + + ), + }; + + if (isPrebuiltRulesCustomizationEnabled) { + return [ + { + id: 'updates', + name: ( + + <>{ruleDetailsI18n.UPDATES_TAB_LABEL} + + ), + content: ( + + + + ), + }, + jsonViewUpdates, + ]; + } + + return [ { id: 'updates', name: ( @@ -236,46 +275,10 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ ), }, - { - id: 'jsonViewUpdates', - name: ( - - <>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL} - - ), - content: ( - - - - ), - }, + jsonViewUpdates, ]; - - if (isPrebuiltRulesCustomizationEnabled) { - extraTabs.unshift({ - id: 'diff', - name: ( - - <>{ruleDetailsI18n.DIFF_TAB_LABEL} - - ), - content: ( - - - - ), - }); - } - - return extraTabs; }, - [rulesUpgradeState, setFieldResolvedValue, isPrebuiltRulesCustomizationEnabled] + [rulesUpgradeState, setRuleFieldResolvedValue, isPrebuiltRulesCustomizationEnabled] ); const filteredRules = useMemo( () => filteredRuleUpgradeInfos.map((rule) => rule.target_rule), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts index 86f2293d312fa..feafc1a6948ab 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts @@ -28,7 +28,7 @@ export interface RuleUpgradeState extends RuleUpgradeInfoForReview { hasUnresolvedConflicts: boolean; } export type RulesUpgradeState = Record; -export type SetFieldResolvedValueFn< +export type SetRuleFieldResolvedValueFn< FieldName extends keyof DiffableAllFields = keyof DiffableAllFields > = (params: { ruleId: RuleObjectId; @@ -41,22 +41,25 @@ type RulesResolvedConflicts = Record; interface UseRulesUpgradeStateResult { rulesUpgradeState: RulesUpgradeState; - setFieldResolvedValue: SetFieldResolvedValueFn; + setRuleFieldResolvedValue: SetRuleFieldResolvedValueFn; } export function usePrebuiltRulesUpgradeState( ruleUpgradeInfos: RuleUpgradeInfoForReview[] ): UseRulesUpgradeStateResult { const [rulesResolvedConflicts, setRulesResolvedConflicts] = useState({}); - const setFieldResolvedValue = useCallback((...[params]: Parameters) => { - setRulesResolvedConflicts((prevRulesResolvedConflicts) => ({ - ...prevRulesResolvedConflicts, - [params.ruleId]: { - ...(prevRulesResolvedConflicts[params.ruleId] ?? {}), - [params.fieldName]: params.resolvedValue, - }, - })); - }, []); + const setRuleFieldResolvedValue = useCallback( + (...[params]: Parameters) => { + setRulesResolvedConflicts((prevRulesResolvedConflicts) => ({ + ...prevRulesResolvedConflicts, + [params.ruleId]: { + ...(prevRulesResolvedConflicts[params.ruleId] ?? {}), + [params.fieldName]: params.resolvedValue, + }, + })); + }, + [] + ); const rulesUpgradeState = useMemo(() => { const state: RulesUpgradeState = {}; @@ -80,7 +83,7 @@ export function usePrebuiltRulesUpgradeState( return { rulesUpgradeState, - setFieldResolvedValue, + setRuleFieldResolvedValue, }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts index e74403777523c..bf87eab2a7293 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts @@ -96,7 +96,7 @@ export const policySettingsMiddlewareRunner: MiddlewareRunner = async ( // Agent summary is secondary data, so its ok for it to come after the details // page is populated with the main content - if (policyItem.policy_id) { + if (policyItem.policy_ids?.length) { const { results } = await sendGetFleetAgentStatusForPolicy(http, policyItem.policy_ids); dispatch({ type: 'serverReturnedPolicyDetailsAgentSummaryData', diff --git a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts index 523b1b9a858b1..c125464bffdb9 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts @@ -83,7 +83,7 @@ export const sendPutPackagePolicy = ( }; /** - * Get a status summary for all Agents that are currently assigned to a given agent policy + * Get a status summary for all Agents that are currently assigned to a given agent policies * * @param http * @param policyIds @@ -91,7 +91,7 @@ export const sendPutPackagePolicy = ( */ export const sendGetFleetAgentStatusForPolicy = ( http: HttpStart, - /** the Agent (fleet) policy id */ + /** the Agent (fleet) policy ids */ policyIds: string[], options: Exclude = {} ): Promise => { diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index 9b4176d070c63..cdafe6b5f8a63 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -85,6 +85,24 @@ export default function ({ getService }: FtrProviderContext) { docCountFormatted: '19 (100%)', }, ], + allFields: [ + '@timestamp', + 'http', + 'http.request', + 'http.request.method', + 'http.response', + 'http.response.body', + 'http.response.body.bytes', + 'http.response.status_code', + 'http.version', + 'message', + 'source', + 'source.address', + 'url', + 'url.original', + 'user_agent', + 'user_agent.original', + ], visibleMetricFieldsCount: 3, totalMetricFieldsCount: 3, populatedFieldsCount: 9, @@ -127,6 +145,7 @@ export default function ({ getService }: FtrProviderContext) { exampleCount: 7, }, ], + allFields: ['Coordinates', 'Location'], visibleMetricFieldsCount: 0, totalMetricFieldsCount: 0, populatedFieldsCount: 3, @@ -171,6 +190,7 @@ export default function ({ getService }: FtrProviderContext) { exampleCount: 3, }, ], + allFields: ['description', 'title', 'value'], visibleMetricFieldsCount: 0, totalMetricFieldsCount: 0, populatedFieldsCount: 3, @@ -207,6 +227,41 @@ export default function ({ getService }: FtrProviderContext) { exampleCount: 11, }, ], + allFields: [ + 'AvgTicketPrice', + 'Cancelled', + 'Carrier', + 'Dest', + 'DestAirportID', + 'DestCityName', + 'DestCountry', + 'DestLocation', + 'DestLocation.lat', + 'DestLocation.lat.keyword', + 'DestLocation.lon', + 'DestLocation.lon.keyword', + 'DestRegion', + 'DestWeather', + 'DistanceKilometers', + 'FlightDelayMin', + 'FlightDelayType', + 'FlightNum', + 'FlightTimeHour', + 'FlightTimeMin', + 'Origin', + 'OriginAirportID', + 'OriginCityName', + 'OriginCountry', + 'OriginLocation', + 'OriginLocation.lat', + 'OriginLocation.lat.keyword', + 'OriginLocation.lon', + 'OriginLocation.lon.keyword', + 'OriginRegion', + 'OriginWeather', + 'dayOfWeek', + 'timestamp', + ], visibleMetricFieldsCount: 0, totalMetricFieldsCount: 0, populatedFieldsCount: 3, @@ -349,6 +404,16 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('closes filebeat config'); await ml.dataVisualizerFileBased.closeCreateFilebeatConfig(); + + await ml.dataVisualizerFileBased.assertDocCountInIndex( + testData.indexName, + testData.expected.ingestedDocCount + ); + + await ml.dataVisualizerFileBased.assertFieldsFromIndex( + testData.indexName, + testData.expected.allFields + ); }); }); } diff --git a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts index df95eddd957f8..d9d59e000d805 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts @@ -16,6 +16,7 @@ export function MachineLearningDataVisualizerFileBasedProvider( { getService, getPageObjects }: FtrProviderContext, mlCommonUI: MlCommonUI ) { + const es = getService('es'); const log = getService('log'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); @@ -177,5 +178,46 @@ export function MachineLearningDataVisualizerFileBasedProvider( await testSubjects.click('fileBeatConfigFlyoutCloseButton'); await testSubjects.missingOrFail('fileDataVisFilebeatConfigPanel'); }, + + async assertDocCountInIndex(index: string, expectedCount: number) { + await retry.tryForTime(60 * 1000, async () => { + const count = await this.getDocCountFromIndex(index); + expect(count).to.eql( + expectedCount, + `Expected document count in index '${index}' to be '${expectedCount}' (got '${count}')` + ); + }); + }, + + async getDocCountFromIndex(index: string) { + const resp = await es.search({ + index, + body: { + size: 0, + query: { + match_all: {}, + }, + }, + }); + // @ts-expect-error incorrect type definition + return resp.hits.total?.value; + }, + + async assertFieldsFromIndex(index: string, fields: string[]) { + await retry.tryForTime(60 * 1000, async () => { + const sortedFields = fields.sort(); + const fieldCaps = await es.fieldCaps({ + index, + fields: '*', + filters: '-metadata', + include_empty_fields: false, + }); + const fieldsFromIndex = Object.keys(fieldCaps.fields).sort(); + expect(fieldsFromIndex).to.eql( + sortedFields, + `Expected fields to be ${sortedFields} (got ${fieldsFromIndex})` + ); + }); + }, }; }