From 20de32ab69ed3e071b27776c0e1af883e4529dbd Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Wed, 29 Jul 2020 21:15:13 +0200 Subject: [PATCH 1/8] Added close button to toast notifications by migrating to different API that is more widely used in Kibana and Security solution in particular. (#73662) * Added close button to toast notifications by migrating to different API that is more widely used in Kibana and Security solution in particular. * Fixed type check errors. * Fixed tests. --- .../endpoint_hosts/view/details/index.tsx | 27 ++++++---------- .../pages/policy/view/policy_details.test.tsx | 12 +++---- .../pages/policy/view/policy_details.tsx | 17 +++++----- .../pages/policy/view/policy_list.tsx | 31 +++++++++---------- 4 files changed, 39 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 212c8977a88526..b22ff406a1605e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -18,7 +18,7 @@ import { import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { useToasts } from '../../../../../common/lib/kibana'; import { useHostSelector } from '../hooks'; import { urlFromQueryParams } from '../url_from_query_params'; import { @@ -44,7 +44,7 @@ import { useFormatUrl } from '../../../../../common/components/link_to'; export const HostDetailsFlyout = memo(() => { const history = useHistory(); - const { notifications } = useKibana(); + const toasts = useToasts(); const queryParams = useHostSelector(uiQueryParams); const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; const details = useHostSelector(detailsData); @@ -58,23 +58,16 @@ export const HostDetailsFlyout = memo(() => { useEffect(() => { if (error !== undefined) { - notifications.toasts.danger({ - title: ( - - ), - body: ( - - ), - toastLifeTimeMs: 10000, + toasts.addDanger({ + title: i18n.translate('xpack.securitySolution.endpoint.host.details.errorTitle', { + defaultMessage: 'Could not find host', + }), + text: i18n.translate('xpack.securitySolution.endpoint.host.details.errorBody', { + defaultMessage: 'Please exit the flyout and select an available host.', + }), }); } - }, [error, notifications.toasts]); + }, [error, toasts]); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 6ed4e06ee51c5c..03ab32dcb2b66b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -255,11 +255,11 @@ describe('Policy Details', () => { policyView.update(); // Toast notification should be shown - const toastAddMock = coreStart.notifications.toasts.add.mock; + const toastAddMock = coreStart.notifications.toasts.addSuccess.mock; expect(toastAddMock.calls).toHaveLength(1); expect(toastAddMock.calls[0][0]).toMatchObject({ - color: 'success', - iconType: 'check', + title: 'Success!', + text: expect.any(Function), }); }); it('should show an error notification toast if update fails', async () => { @@ -270,11 +270,11 @@ describe('Policy Details', () => { policyView.update(); // Toast notification should be shown - const toastAddMock = coreStart.notifications.toasts.add.mock; + const toastAddMock = coreStart.notifications.toasts.addDanger.mock; expect(toastAddMock.calls).toHaveLength(1); expect(toastAddMock.calls[0][0]).toMatchObject({ - color: 'danger', - iconType: 'alert', + title: 'Failed!', + text: expect.any(String), }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index cd63991dbac93f..d309faf59d0443 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -31,11 +31,12 @@ import { isLoading, apiError, } from '../store/policy_details/selectors'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useKibana, toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; +import { useToasts } from '../../../../common/lib/kibana'; import { AppAction } from '../../../../common/store/actions'; import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { PageViewHeaderTitle } from '../../../../common/components/endpoint/page_view'; @@ -51,11 +52,11 @@ import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); const { - notifications, services: { application: { navigateToApp }, }, } = useKibana(); + const toasts = useToasts(); const { formatUrl } = useFormatUrl(SecurityPageName.administration); const { state: locationRouteState } = useLocation(); @@ -76,15 +77,14 @@ export const PolicyDetails = React.memo(() => { useEffect(() => { if (policyUpdateStatus) { if (policyUpdateStatus.success) { - notifications.toasts.success({ - toastLifeTimeMs: 10000, + toasts.addSuccess({ title: i18n.translate( 'xpack.securitySolution.endpoint.policy.details.updateSuccessTitle', { defaultMessage: 'Success!', } ), - body: ( + text: toMountPoint( { navigateToApp(...routeState.onSaveNavigateTo); } } else { - notifications.toasts.danger({ - toastLifeTimeMs: 10000, + toasts.addDanger({ title: i18n.translate('xpack.securitySolution.endpoint.policy.details.updateErrorTitle', { defaultMessage: 'Failed!', }), - body: <>{policyUpdateStatus.error!.message}, + text: policyUpdateStatus.error!.message, }); } } - }, [navigateToApp, notifications.toasts, policyName, policyUpdateStatus, routeState]); + }, [navigateToApp, toasts, policyName, policyUpdateStatus, routeState]); const handleBackToListOnClick = useNavigateByRouterEventHandler(hostListRouterPath); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 667aacd9df3bf9..246dbeb39886fb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -36,6 +36,7 @@ import { CreateStructuredSelector } from '../../../../common/store'; import * as selectors from '../store/policy_list/selectors'; import { usePolicyListSelector } from './policy_hooks'; import { PolicyListAction } from '../store/policy_list'; +import { useToasts } from '../../../../common/lib/kibana'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { Immutable, PolicyData } from '../../../../../common/endpoint/types'; import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; @@ -124,7 +125,8 @@ const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({ const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const PolicyList = React.memo(() => { - const { services, notifications } = useKibana(); + const { services } = useKibana(); + const toasts = useToasts(); const history = useHistory(); const location = useLocation(); const { formatUrl, search } = useFormatUrl(SecurityPageName.administration); @@ -167,13 +169,12 @@ export const PolicyList = React.memo(() => { useEffect(() => { if (apiError) { - notifications.toasts.danger({ + toasts.addDanger({ title: apiError.error, - body: apiError.message, - toastLifeTimeMs: 10000, + text: apiError.message, }); } - }, [apiError, dispatch, notifications.toasts]); + }, [apiError, dispatch, toasts]); // Handle showing update statuses useEffect(() => { @@ -181,31 +182,29 @@ export const PolicyList = React.memo(() => { if (deleteStatus === true) { setPolicyIdToDelete(''); setShowDelete(false); - notifications.toasts.success({ - toastLifeTimeMs: 10000, + toasts.addSuccess({ title: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteSuccessToast', { defaultMessage: 'Success!', }), - body: ( - + text: i18n.translate( + 'xpack.securitySolution.endpoint.policyList.deleteSuccessToastDetails', + { + defaultMessage: 'Policy has been deleted.', + } ), }); } else { - notifications.toasts.danger({ - toastLifeTimeMs: 10000, + toasts.addDanger({ title: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteFailedToast', { defaultMessage: 'Failed!', }), - body: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteFailedToastBody', { + text: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteFailedToastBody', { defaultMessage: 'Failed to delete policy', }), }); } } - }, [notifications.toasts, deleteStatus]); + }, [toasts, deleteStatus]); const paginationSetup = useMemo(() => { return { From 30f469c59bc8cb129d586ccbf0545e4ae03d2f19 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 29 Jul 2020 13:03:35 -0700 Subject: [PATCH 2/8] [DOCS] Changes level offset of monitoring pages (#73573) --- docs/user/index.asciidoc | 5 +- docs/user/monitoring/beats-details.asciidoc | 2 +- .../cluster-alerts-license.asciidoc | 2 - docs/user/monitoring/cluster-alerts.asciidoc | 7 +- .../configuring-monitoring.asciidoc | 6 +- docs/user/monitoring/dashboards.asciidoc | 67 ------------------- .../monitoring/elasticsearch-details.asciidoc | 22 +++--- docs/user/monitoring/gs-index.asciidoc | 31 --------- docs/user/monitoring/index.asciidoc | 40 ++--------- docs/user/monitoring/kibana-details.asciidoc | 2 +- .../user/monitoring/logstash-details.asciidoc | 2 +- .../monitoring/monitoring-details.asciidoc | 4 -- .../monitoring/monitoring-kibana.asciidoc | 2 +- .../monitoring/monitoring-metricbeat.asciidoc | 2 +- .../monitoring-troubleshooting.asciidoc | 8 +-- docs/user/monitoring/viewing-metrics.asciidoc | 2 +- .../user/monitoring/xpack-monitoring.asciidoc | 26 +++++++ 17 files changed, 63 insertions(+), 167 deletions(-) delete mode 100644 docs/user/monitoring/cluster-alerts-license.asciidoc delete mode 100644 docs/user/monitoring/dashboards.asciidoc delete mode 100644 docs/user/monitoring/gs-index.asciidoc delete mode 100644 docs/user/monitoring/monitoring-details.asciidoc create mode 100644 docs/user/monitoring/xpack-monitoring.asciidoc diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index a07d584b4391db..01be8c2e264c5c 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -6,7 +6,10 @@ include::getting-started.asciidoc[] include::setup.asciidoc[] -include::monitoring/configuring-monitoring.asciidoc[] +include::monitoring/configuring-monitoring.asciidoc[leveloffset=+1] +include::monitoring/monitoring-metricbeat.asciidoc[leveloffset=+2] +include::monitoring/viewing-metrics.asciidoc[leveloffset=+2] +include::monitoring/monitoring-kibana.asciidoc[leveloffset=+2] include::security/securing-kibana.asciidoc[] diff --git a/docs/user/monitoring/beats-details.asciidoc b/docs/user/monitoring/beats-details.asciidoc index f4ecb2a74d91ec..3d7a726d2f8a23 100644 --- a/docs/user/monitoring/beats-details.asciidoc +++ b/docs/user/monitoring/beats-details.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[beats-page]] -== Beats Monitoring Metrics += Beats Monitoring Metrics ++++ Beats Metrics ++++ diff --git a/docs/user/monitoring/cluster-alerts-license.asciidoc b/docs/user/monitoring/cluster-alerts-license.asciidoc deleted file mode 100644 index ec86b6f578e19e..00000000000000 --- a/docs/user/monitoring/cluster-alerts-license.asciidoc +++ /dev/null @@ -1,2 +0,0 @@ -NOTE: Watcher must be enabled to view cluster alerts. If you have a Basic -license, Top Cluster Alerts are not displayed. \ No newline at end of file diff --git a/docs/user/monitoring/cluster-alerts.asciidoc b/docs/user/monitoring/cluster-alerts.asciidoc index a58ccc7f7d68de..2945ebc67710cf 100644 --- a/docs/user/monitoring/cluster-alerts.asciidoc +++ b/docs/user/monitoring/cluster-alerts.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[cluster-alerts]] -== Cluster Alerts += Cluster Alerts The *Stack Monitoring > Clusters* page in {kib} summarizes the status of your {stack}. You can drill down into the metrics to view more information about your @@ -39,11 +39,12 @@ valid for 30 days. The {monitor-features} check the cluster alert conditions every minute. Cluster alerts are automatically dismissed when the condition is resolved. -include::cluster-alerts-license.asciidoc[] +NOTE: {watcher} must be enabled to view cluster alerts. If you have a Basic +license, Top Cluster Alerts are not displayed. [float] [[cluster-alert-email-notifications]] -==== Email Notifications +== Email Notifications To receive email notifications for the Cluster Alerts: . Configure an email account as described in diff --git a/docs/user/monitoring/configuring-monitoring.asciidoc b/docs/user/monitoring/configuring-monitoring.asciidoc index 7bcddcac923b20..f158dcc3eee6f4 100644 --- a/docs/user/monitoring/configuring-monitoring.asciidoc +++ b/docs/user/monitoring/configuring-monitoring.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[configuring-monitoring]] -== Configure monitoring in {kib} += Configure monitoring in {kib} ++++ Configure monitoring ++++ @@ -16,7 +16,3 @@ You can also use {kib} to To learn about monitoring in general, see {ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. - -include::monitoring-metricbeat.asciidoc[] -include::viewing-metrics.asciidoc[] -include::monitoring-kibana.asciidoc[] diff --git a/docs/user/monitoring/dashboards.asciidoc b/docs/user/monitoring/dashboards.asciidoc deleted file mode 100644 index 4ffe76f634d938..00000000000000 --- a/docs/user/monitoring/dashboards.asciidoc +++ /dev/null @@ -1,67 +0,0 @@ -[[dashboards]] -== Monitoring's Dashboards - -=== Overview Dashboard - -The _Overview_ dashboard is Monitoring's main page. The dashboard displays the -essentials metrics you need to know that your cluster is healthy. It also -provides an overview of your nodes and indices, displayed in two clean tables -with the relevant key metrics. If some value needs your attention, they will -be highlighted in yellow or red. The nodes and indices tables also serve as an -entry point to the more detailed _Node Statistics_ and _Index Statistics_ -dashboards. - -overview_thumb.png["Overview Dashboard",link="images/overview.png"] - -=== Node & Index Statistics - -The _Node Statistics_ dashboard displays metric charts from the perspective of -one or more nodes. Metrics include hardware level metrics (like load and CPU -usage), process and JVM metrics (memory usage, GC), and node level -Elasticsearch metrics such as field data usage, search requests rate and -thread pool rejection. - -node_stats_thumb.png["Node Statistics Dashboard",link="images/node_stats.png"] - -The _Index Statistics_ dashboard is very similar to the _Node Statistics_ -dashboard, but it shows you all the metrics from the perspective of one or -more indices. The metrics are per index, with data aggregated from all of the -nodes in the cluster. For example, the ''store size'' chart shows the total -size of the index data across the whole cluster. - -index_stats_thumb.png["Index Statistics Dashboard",link="images/index_stats.png"] - -=== Shard Allocation - -The _Shard Allocation_ dashboard displays how the shards are allocated across nodes. -The dashboard also shows the status of the shards. It has two perspectives, _By Indices_ and _By Nodes_. -The _By Indices_ view lists each index and shows you how it's shards are -distributed across nodes. The _By Nodes_ view lists each node and shows you which shards the node current host. - -The viewer also has a playback feature which allows you to view the history of the shard allocation. You can rewind to each -allocation event and then play back the history from any point in time. Hover on relocating shards to highlight both -their previous and new location. The time line color changes based on the state of the cluster for -each time period. - -shard_allocation_thumb.png["Shard Allocation Dashboard",link="images/shard_allocation.png"] - -=== Cluster Pulse - -The Cluster Pulse Dashboard allows you to see any event of interest in the cluster. Typical -events include nodes joining or leaving, master election, index creation, shard (re)allocation -and more. - -cluster_pulse_thumb.png["Index Statistics Dashboard",link="images/cluster_pulse.png"] - -[[sense]] -=== Sense - -_Sense_ is a lightweight developer console. The console is handy when you want -to make an extra API call to check something or perhaps tweak a setting. The -developer console understands both JSON and the Elasticsearch API, offering -suggestions and autocompletion. It is very useful for prototyping queries, -researching your data or any other administrative work with the API. - -image::images/sense_thumb.png["Developer Console",link="sense.png"] - - diff --git a/docs/user/monitoring/elasticsearch-details.asciidoc b/docs/user/monitoring/elasticsearch-details.asciidoc index 15e4676c443dfa..11a561e7ad01f5 100644 --- a/docs/user/monitoring/elasticsearch-details.asciidoc +++ b/docs/user/monitoring/elasticsearch-details.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[elasticsearch-metrics]] -== {es} Monitoring Metrics += {es} Monitoring Metrics [subs="attributes"] ++++ {es} Metrics @@ -18,7 +18,7 @@ See also {ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. [float] [[cluster-overview-page]] -==== Cluster Overview +== Cluster Overview To view the key metrics that indicate the overall health of an {es} cluster, click **Overview** in the {es} section. Anything that needs your attention is @@ -44,7 +44,7 @@ From there, you can dive into detailed metrics for particular nodes and indices. [float] [[nodes-page]] -==== Nodes +== Nodes To view node metrics, click **Nodes**. The Nodes section shows the status of each node in your cluster. @@ -54,7 +54,7 @@ image::user/monitoring/images/monitoring-nodes.png["Elasticsearch Nodes"] [float] [[nodes-page-overview]] -===== Node Overview +=== Node Overview Click the name of a node to view its node statistics over time. These represent high-level statistics collected from {es} that provide a good overview of @@ -66,7 +66,7 @@ image::user/monitoring/images/monitoring-node.png["Elasticsearch Node Overview"] [float] [[nodes-page-advanced]] -===== Node Advanced +=== Node Advanced To view advanced node metrics, click the **Advanced** tab for a node. The *Advanced* tab shows additional metrics, such as memory and garbage collection @@ -81,7 +81,7 @@ more advanced knowledge of {es}, such as poor garbage collection performance. [float] [[indices-overview-page]] -==== Indices +== Indices To view index metrics, click **Indices**. The Indices section shows the same overall index and search metrics as the Overview and a table of your indices. @@ -91,7 +91,7 @@ image::user/monitoring/images/monitoring-indices.png["Elasticsearch Indices"] [float] [[indices-page-overview]] -===== Index Overview +=== Index Overview From the Indices listing, you can view data for a particular index. To drill down into the data for a particular index, click its name in the Indices table. @@ -101,7 +101,7 @@ image::user/monitoring/images/monitoring-index.png["Elasticsearch Index Overview [float] [[indices-page-advanced]] -===== Index Advanced +=== Index Advanced To view advanced index metrics, click the **Advanced** tab for an index. The *Advanced* tab shows additional metrics, such as memory statistics reported @@ -116,7 +116,7 @@ more advanced knowledge of {es}, such as wasteful index memory usage. [float] [[jobs-page]] -==== Jobs +== Jobs To view {ml} job metrics, click **Jobs**. For each job in your cluster, it shows information such as its status, the number of records processed, the size of the @@ -127,7 +127,7 @@ image::user/monitoring/images/monitoring-jobs.png["Machine learning jobs",link=" [float] [[ccr-overview-page]] -==== CCR +== CCR To view {ccr} metrics, click **CCR**. For each follower index on the cluster, it shows information such as the leader index, an indication of how much the @@ -149,7 +149,7 @@ For more information, see {ref}/xpack-ccr.html[{ccr-cap}]. [float] [[logs-monitor-page]] -==== Logs +== Logs If you use {filebeat} to collect log data from your cluster, you can see its recent logs in the *Stack Monitoring* application. The *Clusters* page lists the diff --git a/docs/user/monitoring/gs-index.asciidoc b/docs/user/monitoring/gs-index.asciidoc deleted file mode 100644 index 69c523647393ca..00000000000000 --- a/docs/user/monitoring/gs-index.asciidoc +++ /dev/null @@ -1,31 +0,0 @@ -[[xpack-monitoring]] -= Monitoring the Elastic Stack - -[partintro] --- -The {monitoring} components enable you to easily monitor the Elastic Stack -from {kibana-ref}/introduction.html[Kibana]. -You can view health and performance data for {es}, Logstash, and {kib} in real -time, as well as analyze past performance. - -A monitoring agent runs on each {es}, {kib}, and Logstash instance to collect -and index metrics. - -By default, metrics are indexed within the cluster you are monitoring. -Setting up a dedicated monitoring cluster ensures you can access historical -monitoring data even if the cluster you're -monitoring goes down. It also enables you to monitor multiple clusters -from a central location. - -When you use a dedicated monitoring cluster, the metrics collected by the -Logstash and Kibana monitoring agents are shipped to the Elasticsearch -cluster you're monitoring, which then forwards all of the metrics to -the monitoring cluster. - -//   - -// image:monitoring-architecture.png["Monitoring Architecture",link="images/monitoring-architecture.png"] - --- - -include::getting-started.asciidoc[] diff --git a/docs/user/monitoring/index.asciidoc b/docs/user/monitoring/index.asciidoc index edc572a56434e3..ab773657073bad 100644 --- a/docs/user/monitoring/index.asciidoc +++ b/docs/user/monitoring/index.asciidoc @@ -1,33 +1,7 @@ -[role="xpack"] -[[xpack-monitoring]] -= Stack Monitoring - -[partintro] --- - -The {kib} {monitor-features} serve two separate purposes: - -. To visualize monitoring data from across the {stack}. You can view health and -performance data for {es}, {ls}, and Beats in real time, as well as analyze past -performance. -. To monitor {kib} itself and route that data to the monitoring cluster. - -If you enable monitoring across the {stack}, each {es} node, {ls} node, {kib} -instance, and Beat is considered unique based on its persistent -UUID, which is written to the <> directory when the node -or instance starts. - -NOTE: Watcher must be enabled to view cluster alerts. If you have a Basic -license, Top Cluster Alerts are not displayed. - -For more information, see <> and -{ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. - --- - -include::beats-details.asciidoc[] -include::cluster-alerts.asciidoc[] -include::elasticsearch-details.asciidoc[] -include::kibana-details.asciidoc[] -include::logstash-details.asciidoc[] -include::monitoring-troubleshooting.asciidoc[] +include::xpack-monitoring.asciidoc[] +include::beats-details.asciidoc[leveloffset=+1] +include::cluster-alerts.asciidoc[leveloffset=+1] +include::elasticsearch-details.asciidoc[leveloffset=+1] +include::kibana-details.asciidoc[leveloffset=+1] +include::logstash-details.asciidoc[leveloffset=+1] +include::monitoring-troubleshooting.asciidoc[leveloffset=+1] diff --git a/docs/user/monitoring/kibana-details.asciidoc b/docs/user/monitoring/kibana-details.asciidoc index 976ef456fcfa52..a5466f1418ae89 100644 --- a/docs/user/monitoring/kibana-details.asciidoc +++ b/docs/user/monitoring/kibana-details.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[kibana-page]] -== {kib} Monitoring Metrics += {kib} Monitoring Metrics [subs="attributes"] ++++ {kib} Metrics diff --git a/docs/user/monitoring/logstash-details.asciidoc b/docs/user/monitoring/logstash-details.asciidoc index 1433a6a036ca8a..9d7e3ce342e163 100644 --- a/docs/user/monitoring/logstash-details.asciidoc +++ b/docs/user/monitoring/logstash-details.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[logstash-page]] -== Logstash Monitoring Metrics += Logstash Monitoring Metrics ++++ Logstash Metrics ++++ diff --git a/docs/user/monitoring/monitoring-details.asciidoc b/docs/user/monitoring/monitoring-details.asciidoc deleted file mode 100644 index 580e02d86155a0..00000000000000 --- a/docs/user/monitoring/monitoring-details.asciidoc +++ /dev/null @@ -1,4 +0,0 @@ -[[monitoring-details]] -== Viewing Monitoring Metrics - -See {kibana-ref}/monitoring-data.html[Viewing Monitoring Data in {kib}]. diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index bb8b3e5d42851b..47fbe1bea9f2a6 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[monitoring-kibana]] -=== Collect monitoring data using legacy collectors += Collect monitoring data using legacy collectors ++++ Legacy collection methods ++++ diff --git a/docs/user/monitoring/monitoring-metricbeat.asciidoc b/docs/user/monitoring/monitoring-metricbeat.asciidoc index f2b32ba1de5ddc..d18ebe95c7974d 100644 --- a/docs/user/monitoring/monitoring-metricbeat.asciidoc +++ b/docs/user/monitoring/monitoring-metricbeat.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[monitoring-metricbeat]] -=== Collect {kib} monitoring data with {metricbeat} += Collect {kib} monitoring data with {metricbeat} [subs="attributes"] ++++ Collect monitoring data with {metricbeat} diff --git a/docs/user/monitoring/monitoring-troubleshooting.asciidoc b/docs/user/monitoring/monitoring-troubleshooting.asciidoc index bdaa10990c3aa9..5bec56df0398b7 100644 --- a/docs/user/monitoring/monitoring-troubleshooting.asciidoc +++ b/docs/user/monitoring/monitoring-troubleshooting.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[monitor-troubleshooting]] -== Troubleshooting monitoring in {kib} += Troubleshooting monitoring in {kib} ++++ Troubleshooting ++++ @@ -9,7 +9,7 @@ Use the information in this section to troubleshoot common problems and find answers for frequently asked questions related to the {kib} {monitor-features}. [float] -=== Cannot view the cluster because the license information is invalid +== Cannot view the cluster because the license information is invalid *Symptoms:* @@ -24,7 +24,7 @@ To resolve this issue, upgrade {kib} to 6.3 or later. See {stack-ref}/upgrading-elastic-stack.html[Upgrading the {stack}]. [float] -=== {filebeat} index is corrupt +== {filebeat} index is corrupt *Symptoms:* @@ -41,7 +41,7 @@ text fields by default. [float] -=== No monitoring data is visible in {kib} +== No monitoring data is visible in {kib} *Symptoms:* diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index 6203565c3fe939..f35caea025cdd5 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[monitoring-data]] -=== View monitoring data in {kib} += View monitoring data in {kib} ++++ View monitoring data ++++ diff --git a/docs/user/monitoring/xpack-monitoring.asciidoc b/docs/user/monitoring/xpack-monitoring.asciidoc new file mode 100644 index 00000000000000..c3aafe7f90db9c --- /dev/null +++ b/docs/user/monitoring/xpack-monitoring.asciidoc @@ -0,0 +1,26 @@ +[role="xpack"] +[[xpack-monitoring]] += Stack Monitoring + +[partintro] +-- + +The {kib} {monitor-features} serve two separate purposes: + +. To visualize monitoring data from across the {stack}. You can view health and +performance data for {es}, {ls}, and Beats in real time, as well as analyze past +performance. +. To monitor {kib} itself and route that data to the monitoring cluster. + +If you enable monitoring across the {stack}, each {es} node, {ls} node, {kib} +instance, and Beat is considered unique based on its persistent +UUID, which is written to the <> directory when the node +or instance starts. + +NOTE: Watcher must be enabled to view cluster alerts. If you have a Basic +license, Top Cluster Alerts are not displayed. + +For more information, see <> and +{ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. + +-- \ No newline at end of file From 297a2c61d66f3d5aa4d72ab6fff8df9abe9b77d6 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Wed, 29 Jul 2020 17:21:49 -0400 Subject: [PATCH 3/8] [Canvas][fatal bug] Fix props confusion in TextStylePicker (#73732) Quick fix for a fatal I want to get merged as soon as possible. --- .../__stories__/text_style_picker.stories.tsx | 10 ++-- .../text_style_picker/text_style_picker.tsx | 46 +++++++++++-------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/text_style_picker/__stories__/text_style_picker.stories.tsx b/x-pack/plugins/canvas/public/components/text_style_picker/__stories__/text_style_picker.stories.tsx index b33a34fcd5e65f..7613c834bfc02c 100644 --- a/x-pack/plugins/canvas/public/components/text_style_picker/__stories__/text_style_picker.stories.tsx +++ b/x-pack/plugins/canvas/public/components/text_style_picker/__stories__/text_style_picker.stories.tsx @@ -8,11 +8,15 @@ import React, { useState } from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { TextStylePicker } from '../text_style_picker'; +import { TextStylePicker, StyleProps } from '../text_style_picker'; const Interactive = () => { - const [props, setProps] = useState({}); - return ; + const [style, setStyle] = useState({}); + const onChange = (styleChange: StyleProps) => { + setStyle(styleChange); + action('onChange')(styleChange); + }; + return ; }; storiesOf('components/TextStylePicker', module) diff --git a/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx b/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx index 3dfc55919395d4..c501e78a5e338a 100644 --- a/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx +++ b/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState, useEffect } from 'react'; +import React, { FC, useState } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiSpacer, EuiButtonGroup } from '@elastic/eui'; import { FontValue } from 'src/plugins/expressions'; @@ -15,7 +15,7 @@ import { fontSizes } from './font_sizes'; const { TextStylePicker: strings } = ComponentStrings; -interface BaseProps { +export interface StyleProps { family?: FontValue; size?: number; align?: 'left' | 'center' | 'right'; @@ -25,9 +25,9 @@ interface BaseProps { italic?: boolean; } -interface Props extends BaseProps { +export interface Props extends StyleProps { colors?: string[]; - onChange: (props: BaseProps) => void; + onChange: (style: StyleProps) => void; } type StyleType = 'bold' | 'italic' | 'underline'; @@ -68,20 +68,26 @@ const styleButtons = [ }, ]; -export const TextStylePicker: FC = (props) => { - const [style, setStyle] = useState(props); - - const { - align = 'left', +export const TextStylePicker: FC = ({ + align = 'left', + color, + colors, + family, + italic = false, + onChange, + size = 14, + underline = false, + weight = 'normal', +}) => { + const [style, setStyle] = useState({ + align, color, - colors, family, - italic = false, - onChange, - size = 14, - underline = false, - weight = 'normal', - } = style; + italic, + size, + underline, + weight, + }); const stylesSelectedMap: Record = { ['bold']: weight === 'bold', @@ -94,10 +100,10 @@ export const TextStylePicker: FC = (props) => { fontSizes.sort((a, b) => a - b); } - useEffect(() => onChange(style), [onChange, style]); - - const doChange = (propName: keyof Props, value: string | boolean | number) => { - setStyle({ ...style, [propName]: value }); + const doChange = (propName: keyof StyleProps, value: string | boolean | number) => { + const newStyle = { ...style, [propName]: value }; + setStyle(newStyle); + onChange(newStyle); }; const onStyleChange = (optionId: string) => { From c102c4fe18156be533c9bcdaf41587e53bec6445 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 29 Jul 2020 14:41:22 -0700 Subject: [PATCH 4/8] skip failing suite (#58815) --- test/functional/apps/context/_date_nanos.js | 3 ++- test/functional/apps/context/_date_nanos_custom_timestamp.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index cdf2d6c04be83c..89769caaea2536 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -30,7 +30,8 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - describe('context view for date_nanos', () => { + // FLAKY/FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/58815 + describe.skip('context view for date_nanos', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await esArchiver.loadIfNeeded('date_nanos'); diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index dbfb77c31dff17..6329f6c431e6af 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -29,8 +29,10 @@ export default function ({ getService, getPageObjects }) { const security = getService('security'); const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); + // skipped due to a recent change in ES that caused search_after queries with data containing // custom timestamp formats like in the testdata to fail + // https://github.com/elastic/kibana/issues/58815 describe.skip('context view for date_nanos with custom timestamp', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_custom']); From 0756dd3ae71b582e151dd283a9d470847a3e541f Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 29 Jul 2020 17:51:09 -0400 Subject: [PATCH 5/8] [Security Solution][Exceptions] - Updates exception hooks and viewer (#73588) ## Summary This PR focuses on addressing issues around the pagination and functionality of rules with numerous (2+) exception lists. - Updated the `use_exception_list.ts` hook to make use of the new multi list find API - Updated the viewer to make use of the new multi list find API - Previously was doing a lot of the filtering and paging manually (and badly) in the UI, now the _find takes care of all that - Added logic for showing `No results` text if user filter/search returns no items - Previously would show the `This rule has not exceptions` text --- .../lists/public/exceptions/api.test.ts | 102 ++--- x-pack/plugins/lists/public/exceptions/api.ts | 48 +-- .../public/exceptions/hooks/use_api.test.ts | 115 +++++- .../lists/public/exceptions/hooks/use_api.ts | 57 ++- .../hooks/use_exception_list.test.ts | 352 ++++++++++++++---- .../exceptions/hooks/use_exception_list.ts | 190 +++++----- .../plugins/lists/public/exceptions/types.ts | 22 +- .../lists/public/exceptions/utils.test.ts | 105 ++++++ .../plugins/lists/public/exceptions/utils.ts | 36 ++ .../new/exception_list_item_auto_id.json | 2 +- .../components/exceptions/translations.ts | 14 + .../common/components/exceptions/types.ts | 4 +- .../viewer/exceptions_pagination.test.tsx | 7 +- .../viewer/exceptions_pagination.tsx | 8 +- .../viewer/exceptions_utility.test.tsx | 48 +-- .../exceptions/viewer/exceptions_utility.tsx | 12 +- .../viewer/exceptions_viewer_header.test.tsx | 16 +- .../viewer/exceptions_viewer_header.tsx | 28 +- ...t.tsx => exceptions_viewer_items.test.tsx} | 39 +- .../viewer/exceptions_viewer_items.tsx | 20 +- .../components/exceptions/viewer/index.tsx | 148 +++++--- .../components/exceptions/viewer/reducer.ts | 104 +++--- .../rules/patches/simplest_updated_name.json | 60 ++- 23 files changed, 1114 insertions(+), 423 deletions(-) create mode 100644 x-pack/plugins/lists/public/exceptions/utils.test.ts create mode 100644 x-pack/plugins/lists/public/exceptions/utils.ts rename x-pack/plugins/security_solution/public/common/components/exceptions/viewer/{exceptions_viewer_item.test.tsx => exceptions_viewer_items.test.tsx} (78%) diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 455670098307fd..9add15c533d145 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -26,7 +26,7 @@ import { deleteExceptionListItemById, fetchExceptionListById, fetchExceptionListItemById, - fetchExceptionListItemsByListId, + fetchExceptionListsItemsByListIds, updateExceptionList, updateExceptionListItem, } from './api'; @@ -358,17 +358,18 @@ describe('Exceptions Lists API', () => { }); }); - describe('#fetchExceptionListItemsByListId', () => { + describe('#fetchExceptionListsItemsByListIds', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(getFoundExceptionListItemSchemaMock()); }); - test('it invokes "fetchExceptionListItemsByListId" with expected url and body values', async () => { - await fetchExceptionListItemsByListId({ + test('it invokes "fetchExceptionListsItemsByListIds" with expected url and body values', async () => { + await fetchExceptionListsItemsByListIds({ + filterOptions: [], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'single', + listIds: ['myList', 'myOtherListId'], + namespaceTypes: ['single', 'single'], pagination: { page: 1, perPage: 20, @@ -379,8 +380,8 @@ describe('Exceptions Lists API', () => { expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { - list_id: 'myList', - namespace_type: 'single', + list_id: 'myList,myOtherListId', + namespace_type: 'single,single', page: '1', per_page: '20', }, @@ -389,14 +390,16 @@ describe('Exceptions Lists API', () => { }); test('it invokes with expected url and body values when a filter exists and "namespaceType" of "single"', async () => { - await fetchExceptionListItemsByListId({ - filterOptions: { - filter: 'hello world', - tags: [], - }, + await fetchExceptionListsItemsByListIds({ + filterOptions: [ + { + filter: 'hello world', + tags: [], + }, + ], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'single', + listIds: ['myList'], + namespaceTypes: ['single'], pagination: { page: 1, perPage: 20, @@ -418,14 +421,16 @@ describe('Exceptions Lists API', () => { }); test('it invokes with expected url and body values when a filter exists and "namespaceType" of "agnostic"', async () => { - await fetchExceptionListItemsByListId({ - filterOptions: { - filter: 'hello world', - tags: [], - }, + await fetchExceptionListsItemsByListIds({ + filterOptions: [ + { + filter: 'hello world', + tags: [], + }, + ], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'agnostic', + listIds: ['myList'], + namespaceTypes: ['agnostic'], pagination: { page: 1, perPage: 20, @@ -447,14 +452,16 @@ describe('Exceptions Lists API', () => { }); test('it invokes with expected url and body values when tags exists', async () => { - await fetchExceptionListItemsByListId({ - filterOptions: { - filter: '', - tags: ['malware'], - }, + await fetchExceptionListsItemsByListIds({ + filterOptions: [ + { + filter: '', + tags: ['malware'], + }, + ], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'agnostic', + listIds: ['myList'], + namespaceTypes: ['agnostic'], pagination: { page: 1, perPage: 20, @@ -476,14 +483,16 @@ describe('Exceptions Lists API', () => { }); test('it invokes with expected url and body values when filter and tags exists', async () => { - await fetchExceptionListItemsByListId({ - filterOptions: { - filter: 'host.name', - tags: ['malware'], - }, + await fetchExceptionListsItemsByListIds({ + filterOptions: [ + { + filter: 'host.name', + tags: ['malware'], + }, + ], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'agnostic', + listIds: ['myList'], + namespaceTypes: ['agnostic'], pagination: { page: 1, perPage: 20, @@ -506,10 +515,11 @@ describe('Exceptions Lists API', () => { }); test('it returns expected format when call succeeds', async () => { - const exceptionResponse = await fetchExceptionListItemsByListId({ + const exceptionResponse = await fetchExceptionListsItemsByListIds({ + filterOptions: [], http: mockKibanaHttpService(), - listId: 'endpoint_list_id', - namespaceType: 'single', + listIds: ['endpoint_list_id'], + namespaceTypes: ['single'], pagination: { page: 1, perPage: 20, @@ -521,16 +531,17 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ + filterOptions: [], http: mockKibanaHttpService(), - listId: '1', - namespaceType: 'not a namespace type', + listIds: ['myList'], + namespaceTypes: ['not a namespace type'], pagination: { page: 1, perPage: 20, }, signal: abortCtrl.signal, } as unknown) as ApiCallByListIdProps & { listId: number }; - await expect(fetchExceptionListItemsByListId(payload)).rejects.toEqual( + await expect(fetchExceptionListsItemsByListIds(payload)).rejects.toEqual( 'Invalid value "not a namespace type" supplied to "namespace_type"' ); }); @@ -541,10 +552,11 @@ describe('Exceptions Lists API', () => { fetchMock.mockResolvedValue(badPayload); await expect( - fetchExceptionListItemsByListId({ + fetchExceptionListsItemsByListIds({ + filterOptions: [], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'single', + listIds: ['myList'], + namespaceTypes: ['single'], pagination: { page: 1, perPage: 20, diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index 4d9397ec0adc6c..d661cb103fad80 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -249,42 +249,46 @@ export const fetchExceptionListById = async ({ * Fetch an ExceptionList's ExceptionItems by providing a ExceptionList list_id * * @param http Kibana http service - * @param listId ExceptionList list_id (not ID) - * @param namespaceType ExceptionList namespace_type + * @param listIds ExceptionList list_ids (not ID) + * @param namespaceTypes ExceptionList namespace_types * @param filterOptions optional - filter by field or tags * @param pagination optional * @param signal to cancel request * * @throws An error if response is not OK */ -export const fetchExceptionListItemsByListId = async ({ +export const fetchExceptionListsItemsByListIds = async ({ http, - listId, - namespaceType, - filterOptions = { - filter: '', - tags: [], - }, + listIds, + namespaceTypes, + filterOptions, pagination, signal, }: ApiCallByListIdProps): Promise => { - const namespace = - namespaceType === 'agnostic' ? EXCEPTION_LIST_NAMESPACE_AGNOSTIC : EXCEPTION_LIST_NAMESPACE; - const filters = [ - ...(filterOptions.filter.length - ? [`${namespace}.attributes.entries.field:${filterOptions.filter}*`] - : []), - ...(filterOptions.tags.length - ? filterOptions.tags.map((t) => `${namespace}.attributes.tags:${t}`) - : []), - ]; + const filters: string = filterOptions + .map((filter, index) => { + const namespace = namespaceTypes[index]; + const filterNamespace = + namespace === 'agnostic' ? EXCEPTION_LIST_NAMESPACE_AGNOSTIC : EXCEPTION_LIST_NAMESPACE; + const formattedFilters = [ + ...(filter.filter.length + ? [`${filterNamespace}.attributes.entries.field:${filter.filter}*`] + : []), + ...(filter.tags.length + ? filter.tags.map((t) => `${filterNamespace}.attributes.tags:${t}`) + : []), + ]; + + return formattedFilters.join(' AND '); + }) + .join(','); const query = { - list_id: listId, - namespace_type: namespaceType, + list_id: listIds.join(','), + namespace_type: namespaceTypes.join(','), page: pagination.page ? `${pagination.page}` : '1', per_page: pagination.perPage ? `${pagination.perPage}` : '20', - ...(filters.length ? { filter: filters.join(' AND ') } : {}), + ...(filters.trim() !== '' ? { filter: filters } : {}), }; const [validatedRequest, errorsRequest] = validate(query, findExceptionListItemSchema); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts index 1e0f7e58a0f4cf..c93155274937e1 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts @@ -9,9 +9,10 @@ import { act, renderHook } from '@testing-library/react-hooks'; import * as api from '../api'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; +import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; import { HttpStart } from '../../../../../../src/core/public'; -import { ApiCallByIdProps } from '../types'; +import { ApiCallByIdProps, ApiCallByListIdProps } from '../types'; import { ExceptionsApi, useApi } from './use_api'; @@ -252,4 +253,116 @@ describe('useApi', () => { expect(onErrorMock).toHaveBeenCalledWith(mockError); }); }); + + test('it invokes "fetchExceptionListsItemsByListIds" when "getExceptionItem" used', async () => { + const output = getFoundExceptionListItemSchemaMock(); + const onSuccessMock = jest.fn(); + const spyOnFetchExceptionListsItemsByListIds = jest + .spyOn(api, 'fetchExceptionListsItemsByListIds') + .mockResolvedValue(output); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + await result.current.getExceptionListsItems({ + filterOptions: [], + lists: [{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }], + onError: jest.fn(), + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, + }); + + const expected: ApiCallByListIdProps = { + filterOptions: [], + http: mockKibanaHttpService, + listIds: ['list_id'], + namespaceTypes: ['single'], + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + signal: new AbortController().signal, + }; + + expect(spyOnFetchExceptionListsItemsByListIds).toHaveBeenCalledWith(expected); + expect(onSuccessMock).toHaveBeenCalled(); + }); + }); + + test('it does not invoke "fetchExceptionListsItemsByListIds" if no listIds', async () => { + const output = getFoundExceptionListItemSchemaMock(); + const onSuccessMock = jest.fn(); + const spyOnFetchExceptionListsItemsByListIds = jest + .spyOn(api, 'fetchExceptionListsItemsByListIds') + .mockResolvedValue(output); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + await result.current.getExceptionListsItems({ + filterOptions: [], + lists: [{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }], + onError: jest.fn(), + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, + }); + + expect(spyOnFetchExceptionListsItemsByListIds).not.toHaveBeenCalled(); + expect(onSuccessMock).toHaveBeenCalledWith({ + exceptions: [], + pagination: { + page: 0, + perPage: 20, + total: 0, + }, + }); + }); + }); + + test('invokes "onError" callback if "fetchExceptionListsItemsByListIds" fails', async () => { + const mockError = new Error('failed to delete item'); + jest.spyOn(api, 'fetchExceptionListsItemsByListIds').mockRejectedValue(mockError); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + await result.current.getExceptionListsItems({ + filterOptions: [], + lists: [{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }], + onError: onErrorMock, + onSuccess: jest.fn(), + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, + }); + + expect(onErrorMock).toHaveBeenCalledWith(mockError); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts index 45e180d9d617c6..def2f2626b8ec3 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts @@ -9,7 +9,8 @@ import { useMemo } from 'react'; import * as Api from '../api'; import { HttpStart } from '../../../../../../src/core/public'; import { ExceptionListItemSchema, ExceptionListSchema } from '../../../common/schemas'; -import { ApiCallMemoProps } from '../types'; +import { ApiCallFindListsItemsMemoProps, ApiCallMemoProps } from '../types'; +import { getIdsAndNamespaces } from '../utils'; export interface ExceptionsApi { deleteExceptionItem: (arg: ApiCallMemoProps) => Promise; @@ -20,6 +21,7 @@ export interface ExceptionsApi { getExceptionList: ( arg: ApiCallMemoProps & { onSuccess: (arg: ExceptionListSchema) => void } ) => Promise; + getExceptionListsItems: (arg: ApiCallFindListsItemsMemoProps) => Promise; } export const useApi = (http: HttpStart): ExceptionsApi => { @@ -105,6 +107,59 @@ export const useApi = (http: HttpStart): ExceptionsApi => { onError(error); } }, + async getExceptionListsItems({ + lists, + filterOptions, + pagination, + showDetectionsListsOnly, + showEndpointListsOnly, + onSuccess, + onError, + }: ApiCallFindListsItemsMemoProps): Promise { + const abortCtrl = new AbortController(); + const { ids, namespaces } = getIdsAndNamespaces({ + lists, + showDetection: showDetectionsListsOnly, + showEndpoint: showEndpointListsOnly, + }); + + try { + if (ids.length > 0 && namespaces.length > 0) { + const { + data, + page, + per_page: perPage, + total, + } = await Api.fetchExceptionListsItemsByListIds({ + filterOptions, + http, + listIds: ids, + namespaceTypes: namespaces, + pagination, + signal: abortCtrl.signal, + }); + onSuccess({ + exceptions: data, + pagination: { + page, + perPage, + total, + }, + }); + } else { + onSuccess({ + exceptions: [], + pagination: { + page: 0, + perPage: pagination.perPage ?? 0, + total: 0, + }, + }); + } + } catch (error) { + onError(error); + } + }, }), [http] ); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts index f678ed4faeeda0..3a8b1713b901be 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts @@ -8,10 +8,9 @@ import { act, renderHook } from '@testing-library/react-hooks'; import * as api from '../api'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; -import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; import { ExceptionListItemSchema } from '../../../common/schemas'; -import { ExceptionList, UseExceptionListProps, UseExceptionListSuccess } from '../types'; +import { UseExceptionListProps, UseExceptionListSuccess } from '../types'; import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list'; @@ -21,9 +20,8 @@ describe('useExceptionList', () => { const onErrorMock = jest.fn(); beforeEach(() => { - jest.spyOn(api, 'fetchExceptionListById').mockResolvedValue(getExceptionListSchemaMock()); jest - .spyOn(api, 'fetchExceptionListItemsByListId') + .spyOn(api, 'fetchExceptionListsItemsByListIds') .mockResolvedValue(getFoundExceptionListItemSchemaMock()); }); @@ -39,17 +37,20 @@ describe('useExceptionList', () => { ReturnExceptionListAndItems >(() => useExceptionList({ - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, pagination: { page: 1, perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }) ); await waitForNextUpdate(); @@ -57,7 +58,6 @@ describe('useExceptionList', () => { expect(result.current).toEqual([ true, [], - [], { page: 1, perPage: 20, @@ -68,7 +68,7 @@ describe('useExceptionList', () => { }); }); - test('fetch exception list and items', async () => { + test('fetches exception items', async () => { await act(async () => { const onSuccessMock = jest.fn(); const { result, waitForNextUpdate } = renderHook< @@ -76,11 +76,12 @@ describe('useExceptionList', () => { ReturnExceptionListAndItems >(() => useExceptionList({ - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, onSuccess: onSuccessMock, pagination: { @@ -88,56 +89,279 @@ describe('useExceptionList', () => { perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }) ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params await waitForNextUpdate(); await waitForNextUpdate(); - const expectedListResult: ExceptionList[] = [ - { ...getExceptionListSchemaMock(), totalItems: 1 }, - ]; - const expectedListItemsResult: ExceptionListItemSchema[] = getFoundExceptionListItemSchemaMock() .data; const expectedResult: UseExceptionListSuccess = { exceptions: expectedListItemsResult, - lists: expectedListResult, pagination: { page: 1, perPage: 1, total: 1 }, }; expect(result.current).toEqual([ false, - expectedListResult, expectedListItemsResult, { page: 1, perPage: 1, total: 1, }, - result.current[4], + result.current[3], ]); expect(onSuccessMock).toHaveBeenCalledWith(expectedResult); }); }); - test('fetch a new exception list and its items', async () => { - const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); - const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId'); + test('fetches only detection list items if "showDetectionsListsOnly" is true', async () => { + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); + + await act(async () => { + const onSuccessMock = jest.fn(); + const { waitForNextUpdate } = renderHook( + () => + useExceptionList({ + filterOptions: [], + http: mockKibanaHttpService, + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + matchFilters: false, + onError: onErrorMock, + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: true, + showEndpointListsOnly: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ + filterOptions: [], + http: mockKibanaHttpService, + listIds: ['list_id'], + namespaceTypes: ['single'], + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('fetches only detection list items if "showEndpointListsOnly" is true', async () => { + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); + + await act(async () => { + const onSuccessMock = jest.fn(); + const { waitForNextUpdate } = renderHook( + () => + useExceptionList({ + filterOptions: [], + http: mockKibanaHttpService, + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + matchFilters: false, + onError: onErrorMock, + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ + filterOptions: [], + http: mockKibanaHttpService, + listIds: ['list_id_endpoint'], + namespaceTypes: ['agnostic'], + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('does not fetch items if no lists to fetch', async () => { + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); + + await act(async () => { + const onSuccessMock = jest.fn(); + const { result, waitForNextUpdate } = renderHook< + UseExceptionListProps, + ReturnExceptionListAndItems + >(() => + useExceptionList({ + filterOptions: [], + http: mockKibanaHttpService, + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + ], + matchFilters: false, + onError: onErrorMock, + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListsItemsByListIds).not.toHaveBeenCalled(); + expect(result.current).toEqual([ + false, + [], + { + page: 0, + perPage: 20, + total: 0, + }, + result.current[3], + ]); + }); + }); + + test('applies first filterOptions filter to all lists if "matchFilters" is true', async () => { + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); + + await act(async () => { + const onSuccessMock = jest.fn(); + const { waitForNextUpdate } = renderHook( + () => + useExceptionList({ + filterOptions: [{ filter: 'host.name', tags: [] }], + http: mockKibanaHttpService, + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + matchFilters: true, + onError: onErrorMock, + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ + filterOptions: [ + { filter: 'host.name', tags: [] }, + { filter: 'host.name', tags: [] }, + ], + http: mockKibanaHttpService, + listIds: ['list_id', 'list_id_endpoint'], + namespaceTypes: ['single', 'agnostic'], + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('fetches a new exception list and its items', async () => { + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); const onSuccessMock = jest.fn(); await act(async () => { const { rerender, waitForNextUpdate } = renderHook< UseExceptionListProps, ReturnExceptionListAndItems >( - ({ filterOptions, http, lists, pagination, onError, onSuccess }) => - useExceptionList({ filterOptions, http, lists, onError, onSuccess, pagination }), + ({ + filterOptions, + http, + lists, + matchFilters, + pagination, + onError, + onSuccess, + showDetectionsListsOnly, + showEndpointListsOnly, + }) => + useExceptionList({ + filterOptions, + http, + lists, + matchFilters, + onError, + onSuccess, + pagination, + showDetectionsListsOnly, + showEndpointListsOnly, + }), { initialProps: { - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, onSuccess: onSuccessMock, pagination: { @@ -145,16 +369,23 @@ describe('useExceptionList', () => { perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }, } ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params await waitForNextUpdate(); + await waitForNextUpdate(); + rerender({ - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'newListId', listId: 'new_list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, onSuccess: onSuccessMock, pagination: { @@ -162,109 +393,92 @@ describe('useExceptionList', () => { perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }); + // NOTE: Only need one call here because hook already initilaized await waitForNextUpdate(); - expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2); - expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2); + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(2); }); }); test('fetches list and items when refreshExceptionList callback invoked', async () => { - const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); - const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId'); + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); await act(async () => { const { result, waitForNextUpdate } = renderHook< UseExceptionListProps, ReturnExceptionListAndItems >(() => useExceptionList({ - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, pagination: { page: 1, perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }) ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params await waitForNextUpdate(); await waitForNextUpdate(); - expect(typeof result.current[4]).toEqual('function'); + expect(typeof result.current[3]).toEqual('function'); - if (result.current[4] != null) { - result.current[4](); + if (result.current[3] != null) { + result.current[3](); } - + // NOTE: Only need one call here because hook already initilaized await waitForNextUpdate(); - expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2); - expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2); + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(2); }); }); - test('invokes "onError" callback if "fetchExceptionListItemsByListId" fails', async () => { - const mockError = new Error('failed to fetch list items'); - const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); - const spyOnfetchExceptionListItemsByListId = jest - .spyOn(api, 'fetchExceptionListItemsByListId') + test('invokes "onError" callback if "fetchExceptionListsItemsByListIds" fails', async () => { + const mockError = new Error('failed to fetches list items'); + const spyOnfetchExceptionListsItemsByListIds = jest + .spyOn(api, 'fetchExceptionListsItemsByListIds') .mockRejectedValue(mockError); await act(async () => { const { waitForNextUpdate } = renderHook( () => useExceptionList({ - filterOptions: { filter: '', tags: [] }, - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - onError: onErrorMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - }) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(1); - expect(onErrorMock).toHaveBeenCalledWith(mockError); - expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(1); - }); - }); - - test('invokes "onError" callback if "fetchExceptionListById" fails', async () => { - const mockError = new Error('failed to fetch list'); - jest.spyOn(api, 'fetchExceptionListById').mockRejectedValue(mockError); - - await act(async () => { - const { waitForNextUpdate } = renderHook( - () => - useExceptionList({ - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, pagination: { page: 1, perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }) ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params await waitForNextUpdate(); await waitForNextUpdate(); expect(onErrorMock).toHaveBeenCalledWith(mockError); + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts index c639dcff8b5372..8097a7b8c5898e 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; -import { fetchExceptionListById, fetchExceptionListItemsByListId } from '../api'; -import { ExceptionIdentifiers, ExceptionList, Pagination, UseExceptionListProps } from '../types'; -import { ExceptionListItemSchema, NamespaceType } from '../../../common/schemas'; +import { fetchExceptionListsItemsByListIds } from '../api'; +import { FilterExceptionsOptions, Pagination, UseExceptionListProps } from '../types'; +import { ExceptionListItemSchema } from '../../../common/schemas'; +import { getIdsAndNamespaces } from '../utils'; type Func = () => void; export type ReturnExceptionListAndItems = [ boolean, - ExceptionList[], ExceptionListItemSchema[], Pagination, Func | null @@ -27,6 +27,10 @@ export type ReturnExceptionListAndItems = [ * @param onError error callback * @param onSuccess callback when all lists fetched successfully * @param filterOptions optional - filter by fields or tags + * @param showDetectionsListsOnly boolean, if true, only detection lists are searched + * @param showEndpointListsOnly boolean, if true, only endpoint lists are searched + * @param matchFilters boolean, if true, applies first filter in filterOptions to + * all lists * @param pagination optional * */ @@ -38,134 +42,112 @@ export const useExceptionList = ({ perPage: 20, total: 0, }, - filterOptions = { - filter: '', - tags: [], - }, + filterOptions, + showDetectionsListsOnly, + showEndpointListsOnly, + matchFilters, onError, onSuccess, }: UseExceptionListProps): ReturnExceptionListAndItems => { - const [exceptionLists, setExceptionLists] = useState([]); const [exceptionItems, setExceptionListItems] = useState([]); const [paginationInfo, setPagination] = useState(pagination); - const fetchExceptionList = useRef(null); + const fetchExceptionListsItems = useRef(null); const [loading, setLoading] = useState(true); - const tags = useMemo(() => filterOptions.tags.sort().join(), [filterOptions.tags]); - const listIds = useMemo( - () => - lists - .map((t) => t.id) - .sort() - .join(), - [lists] - ); + const { ids, namespaces } = getIdsAndNamespaces({ + lists, + showDetection: showDetectionsListsOnly, + showEndpoint: showEndpointListsOnly, + }); + const filters: FilterExceptionsOptions[] = + matchFilters && filterOptions.length > 0 ? ids.map(() => filterOptions[0]) : filterOptions; + const idsAsString: string = ids.join(','); + const namespacesAsString: string = namespaces.join(','); + const filterAsString: string = filterOptions.map(({ filter }) => filter).join(','); + const filterTagsAsString: string = filterOptions.map(({ tags }) => tags.join(',')).join(','); useEffect( () => { - let isSubscribed = false; - let abortCtrl: AbortController; - - const fetchLists = async (): Promise => { - isSubscribed = true; - abortCtrl = new AbortController(); - - // TODO: workaround until api updated, will be cleaned up - let exceptions: ExceptionListItemSchema[] = []; - let exceptionListsReturned: ExceptionList[] = []; - - const fetchData = async ({ - id, - namespaceType, - }: { - id: string; - namespaceType: NamespaceType; - }): Promise => { - try { - setLoading(true); - - const { - list_id, - namespace_type, - ...restOfExceptionList - } = await fetchExceptionListById({ - http, - id, - namespaceType, - signal: abortCtrl.signal, + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const fetchData = async (): Promise => { + try { + setLoading(true); + + if (ids.length === 0 && isSubscribed) { + setPagination({ + page: 0, + perPage: pagination.perPage, + total: 0, }); - const fetchListItemsResult = await fetchExceptionListItemsByListId({ - filterOptions, + setExceptionListItems([]); + + if (onSuccess != null) { + onSuccess({ + exceptions: [], + pagination: { + page: 0, + perPage: pagination.perPage, + total: 0, + }, + }); + } + setLoading(false); + } else { + const { page, per_page, total, data } = await fetchExceptionListsItemsByListIds({ + filterOptions: filters, http, - listId: list_id, - namespaceType: namespace_type, - pagination, + listIds: ids, + namespaceTypes: namespaces, + pagination: { + page: pagination.page, + perPage: pagination.perPage, + }, signal: abortCtrl.signal, }); if (isSubscribed) { - exceptionListsReturned = [ - ...exceptionListsReturned, - { - list_id, - namespace_type, - ...restOfExceptionList, - totalItems: fetchListItemsResult.total, - }, - ]; - setExceptionLists(exceptionListsReturned); setPagination({ - page: fetchListItemsResult.page, - perPage: fetchListItemsResult.per_page, - total: fetchListItemsResult.total, + page, + perPage: per_page, + total, }); - - exceptions = [...exceptions, ...fetchListItemsResult.data]; - setExceptionListItems(exceptions); + setExceptionListItems(data); if (onSuccess != null) { onSuccess({ - exceptions, - lists: exceptionListsReturned, + exceptions: data, pagination: { - page: fetchListItemsResult.page, - perPage: fetchListItemsResult.per_page, - total: fetchListItemsResult.total, + page, + perPage: per_page, + total, }, }); } } - } catch (error) { - if (isSubscribed) { - setExceptionLists([]); - setExceptionListItems([]); - setPagination({ - page: 1, - perPage: 20, - total: 0, - }); - if (onError != null) { - onError(error); - } + } + } catch (error) { + if (isSubscribed) { + setExceptionListItems([]); + setPagination({ + page: 1, + perPage: 20, + total: 0, + }); + if (onError != null) { + onError(error); } } - }; - - // TODO: Workaround for now. Once api updated, we can pass in array of lists to fetch - await Promise.all( - lists.map( - ({ id, namespaceType }: ExceptionIdentifiers): Promise => - fetchData({ id, namespaceType }) - ) - ); + } if (isSubscribed) { setLoading(false); } }; - fetchLists(); + fetchData(); - fetchExceptionList.current = fetchLists; + fetchExceptionListsItems.current = fetchData; return (): void => { isSubscribed = false; abortCtrl.abort(); @@ -173,15 +155,15 @@ export const useExceptionList = ({ }, // eslint-disable-next-line react-hooks/exhaustive-deps [ http, - listIds, - setExceptionLists, + idsAsString, + namespacesAsString, setExceptionListItems, pagination.page, pagination.perPage, - filterOptions.filter, - tags, + filterAsString, + filterTagsAsString, ] ); - return [loading, exceptionLists, exceptionItems, paginationInfo, fetchExceptionList.current]; + return [loading, exceptionItems, paginationInfo, fetchExceptionListsItems.current]; }; diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index c0ec72e1c19eb2..ac21288848154c 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -44,7 +44,6 @@ export interface ExceptionList extends ExceptionListSchema { } export interface UseExceptionListSuccess { - lists: ExceptionList[]; exceptions: ExceptionListItemSchema[]; pagination: Pagination; } @@ -53,8 +52,11 @@ export interface UseExceptionListProps { http: HttpStart; lists: ExceptionIdentifiers[]; onError?: (arg: string[]) => void; - filterOptions?: FilterExceptionsOptions; + filterOptions: FilterExceptionsOptions[]; pagination?: Pagination; + showDetectionsListsOnly: boolean; + showEndpointListsOnly: boolean; + matchFilters: boolean; onSuccess?: (arg: UseExceptionListSuccess) => void; } @@ -67,9 +69,9 @@ export interface ExceptionIdentifiers { export interface ApiCallByListIdProps { http: HttpStart; - listId: string; - namespaceType: NamespaceType; - filterOptions?: FilterExceptionsOptions; + listIds: string[]; + namespaceTypes: NamespaceType[]; + filterOptions: FilterExceptionsOptions[]; pagination: Partial; signal: AbortSignal; } @@ -88,6 +90,16 @@ export interface ApiCallMemoProps { onSuccess: () => void; } +export interface ApiCallFindListsItemsMemoProps { + lists: ExceptionIdentifiers[]; + filterOptions: FilterExceptionsOptions[]; + pagination: Partial; + showDetectionsListsOnly: boolean; + showEndpointListsOnly: boolean; + onError: (arg: string[]) => void; + onSuccess: (arg: UseExceptionListSuccess) => void; +} + export interface AddExceptionListProps { http: HttpStart; list: CreateExceptionListSchema; diff --git a/x-pack/plugins/lists/public/exceptions/utils.test.ts b/x-pack/plugins/lists/public/exceptions/utils.test.ts new file mode 100644 index 00000000000000..cc1a96132b045f --- /dev/null +++ b/x-pack/plugins/lists/public/exceptions/utils.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getIdsAndNamespaces } from './utils'; + +describe('Exceptions utils', () => { + describe('#getIdsAndNamespaces', () => { + test('it returns empty arrays if no lists found', async () => { + const output = getIdsAndNamespaces({ + lists: [], + showDetection: false, + showEndpoint: false, + }); + + expect(output).toEqual({ ids: [], namespaces: [] }); + }); + + test('it returns all lists if "showDetection" and "showEndpoint" are "false"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: false, + showEndpoint: false, + }); + + expect(output).toEqual({ + ids: ['list_id', 'list_id_endpoint'], + namespaces: ['single', 'agnostic'], + }); + }); + + test('it returns only detections lists if "showDetection" is "true"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: true, + showEndpoint: false, + }); + + expect(output).toEqual({ + ids: ['list_id'], + namespaces: ['single'], + }); + }); + + test('it returns only endpoint lists if "showEndpoint" is "true"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: false, + showEndpoint: true, + }); + + expect(output).toEqual({ + ids: ['list_id_endpoint'], + namespaces: ['agnostic'], + }); + }); + + test('it returns only detection lists if both "showEndpoint" and "showDetection" are "true"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: true, + showEndpoint: true, + }); + + expect(output).toEqual({ + ids: ['list_id'], + namespaces: ['single'], + }); + }); + }); +}); diff --git a/x-pack/plugins/lists/public/exceptions/utils.ts b/x-pack/plugins/lists/public/exceptions/utils.ts new file mode 100644 index 00000000000000..2acb690d3822c2 --- /dev/null +++ b/x-pack/plugins/lists/public/exceptions/utils.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { NamespaceType } from '../../common/schemas'; + +import { ExceptionIdentifiers } from './types'; + +export const getIdsAndNamespaces = ({ + lists, + showDetection, + showEndpoint, +}: { + lists: ExceptionIdentifiers[]; + showDetection: boolean; + showEndpoint: boolean; +}): { ids: string[]; namespaces: NamespaceType[] } => + lists + .filter((list) => { + if (showDetection) { + return list.type === 'detection'; + } else if (showEndpoint) { + return list.type === 'endpoint'; + } else { + return true; + } + }) + .reduce<{ ids: string[]; namespaces: NamespaceType[] }>( + (acc, { listId, namespaceType }) => ({ + ids: [...acc.ids, listId], + namespaces: [...acc.namespaces, namespaceType], + }), + { ids: [], namespaces: [] } + ); diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json index d63adc84a361db..f1281e2ea0560c 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json @@ -1,5 +1,5 @@ { - "list_id": "endpoint_list", + "list_id": "simple_list", "_tags": ["endpoint", "process", "malware", "os:linux"], "tags": ["user added string for a tag", "malware"], "type": "simple", diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index 87d2f9dcda9351..b826c1e49f2749 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -95,6 +95,13 @@ export const EXCEPTION_EMPTY_PROMPT_TITLE = i18n.translate( } ); +export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody', + { + defaultMessage: 'No search results found.', + } +); + export const EXCEPTION_EMPTY_PROMPT_BODY = i18n.translate( 'xpack.securitySolution.exceptions.viewer.emptyPromptBody', { @@ -176,3 +183,10 @@ export const ADD_TO_CLIPBOARD = i18n.translate( export const DESCRIPTION = i18n.translate('xpack.securitySolution.exceptions.descriptionLabel', { defaultMessage: 'Description', }); + +export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.fetchTotalsError', + { + defaultMessage: 'Error getting exception item totals', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 54caab03e615a2..83367e5b9e7399 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -38,14 +38,14 @@ export interface ExceptionListItemIdentifiers { export interface FilterOptions { filter: string; - showDetectionsList: boolean; - showEndpointList: boolean; tags: string[]; } export interface Filter { filter: Partial; pagination: Partial; + showDetectionsListsOnly: boolean; + showEndpointListsOnly: boolean; } export interface ExceptionsPagination { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx index dcc8611cd7298b..768af7b837d9b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx @@ -80,7 +80,6 @@ describe('ExceptionsViewerPagination', () => { wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(0).simulate('click'); expect(mockOnPaginationChange).toHaveBeenCalledWith({ - filter: {}, pagination: { pageIndex: 0, pageSize: 20, totalItemCount: 1 }, }); }); @@ -127,8 +126,7 @@ describe('ExceptionsViewerPagination', () => { wrapper.find('[data-test-subj="pagination-button-next"]').at(1).simulate('click'); expect(mockOnPaginationChange).toHaveBeenCalledWith({ - filter: {}, - pagination: { pageIndex: 2, pageSize: 50, totalItemCount: 160 }, + pagination: { pageIndex: 1, pageSize: 50, totalItemCount: 160 }, }); }); @@ -151,8 +149,7 @@ describe('ExceptionsViewerPagination', () => { wrapper.find('button[data-test-subj="pagination-button-3"]').simulate('click'); expect(mockOnPaginationChange).toHaveBeenCalledWith({ - filter: {}, - pagination: { pageIndex: 4, pageSize: 50, totalItemCount: 160 }, + pagination: { pageIndex: 3, pageSize: 50, totalItemCount: 160 }, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx index afc6d55de364d7..ae1a7771164410 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx @@ -20,7 +20,7 @@ import { ExceptionsPagination, Filter } from '../types'; interface ExceptionsViewerPaginationProps { pagination: ExceptionsPagination; - onPaginationChange: (arg: Filter) => void; + onPaginationChange: (arg: Partial) => void; } const ExceptionsViewerPaginationComponent = ({ @@ -39,9 +39,8 @@ const ExceptionsViewerPaginationComponent = ({ const handlePageClick = useCallback( (pageIndex: number): void => { onPaginationChange({ - filter: {}, pagination: { - pageIndex: pageIndex + 1, + pageIndex, pageSize: pagination.pageSize, totalItemCount: pagination.totalItemCount, }, @@ -57,9 +56,8 @@ const ExceptionsViewerPaginationComponent = ({ icon="empty" onClick={() => { onPaginationChange({ - filter: {}, pagination: { - pageIndex: pagination.pageIndex, + pageIndex: 0, pageSize: rows, totalItemCount: pagination.totalItemCount, }, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx index d697023b2ced4e..6927ecec788fb2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx @@ -22,12 +22,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 2, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: false, - showDetectionsList: false, - tags: [], - }} + showEndpointListsOnly={false} + showDetectionsListsOnly={false} ruleSettingsUrl={'some/url'} onRefreshClick={jest.fn()} /> @@ -49,12 +45,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: false, - showDetectionsList: false, - tags: [], - }} + showEndpointListsOnly={false} + showDetectionsListsOnly={false} ruleSettingsUrl={'some/url'} onRefreshClick={jest.fn()} /> @@ -77,12 +69,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: false, - showDetectionsList: false, - tags: [], - }} + showEndpointListsOnly={false} + showDetectionsListsOnly={false} ruleSettingsUrl={'some/url'} onRefreshClick={mockOnRefreshClick} /> @@ -104,12 +92,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: false, - showDetectionsList: false, - tags: [], - }} + showEndpointListsOnly={false} + showDetectionsListsOnly={false} ruleSettingsUrl={'some/url'} onRefreshClick={jest.fn()} /> @@ -130,12 +114,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: false, - showDetectionsList: true, - tags: [], - }} + showEndpointListsOnly={false} + showDetectionsListsOnly ruleSettingsUrl={'some/url'} onRefreshClick={jest.fn()} /> @@ -156,12 +136,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: true, - showDetectionsList: false, - tags: [], - }} + showEndpointListsOnly + showDetectionsListsOnly={false} ruleSettingsUrl={'some/url'} onRefreshClick={jest.fn()} /> diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx index 9ab4e170f4090f..206983f3d82d97 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from 'react-intl'; import styled from 'styled-components'; import * as i18n from '../translations'; -import { ExceptionsPagination, FilterOptions } from '../types'; +import { ExceptionsPagination } from '../types'; import { UtilityBar, UtilityBarSection, @@ -29,14 +29,16 @@ const MyUtilities = styled(EuiFlexGroup)` interface ExceptionsViewerUtilityProps { pagination: ExceptionsPagination; - filterOptions: FilterOptions; + showEndpointListsOnly: boolean; + showDetectionsListsOnly: boolean; ruleSettingsUrl: string; onRefreshClick: () => void; } const ExceptionsViewerUtilityComponent: React.FC = ({ pagination, - filterOptions, + showEndpointListsOnly, + showDetectionsListsOnly, ruleSettingsUrl, onRefreshClick, }): JSX.Element => ( @@ -65,7 +67,7 @@ const ExceptionsViewerUtilityComponent: React.FC = - {filterOptions.showEndpointList && ( + {showEndpointListsOnly && ( = }} /> )} - {filterOptions.showDetectionsList && ( + {showDetectionsListsOnly && ( { expect(mockOnFilterChange).toHaveBeenCalledWith({ filter: { filter: '', - showDetectionsList: true, - showEndpointList: false, tags: [], }, - pagination: {}, + pagination: { + pageIndex: 0, + }, + showDetectionsListsOnly: true, + showEndpointListsOnly: false, }); }); @@ -175,11 +177,13 @@ describe('ExceptionsViewerHeader', () => { expect(mockOnFilterChange).toHaveBeenCalledWith({ filter: { filter: '', - showDetectionsList: false, - showEndpointList: true, tags: [], }, - pagination: {}, + pagination: { + pageIndex: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx index c207f91f651ed3..9cbe5f0f36891a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx @@ -26,7 +26,7 @@ interface ExceptionsViewerHeaderProps { supportedListTypes: ExceptionListTypeEnum[]; detectionsListItems: number; endpointListItems: number; - onFilterChange: (arg: Filter) => void; + onFilterChange: (arg: Partial) => void; onAddExceptionClick: (type: ExceptionListTypeEnum) => void; } @@ -43,16 +43,20 @@ const ExceptionsViewerHeaderComponent = ({ }: ExceptionsViewerHeaderProps): JSX.Element => { const [filter, setFilter] = useState(''); const [tags, setTags] = useState([]); - const [showDetectionsList, setShowDetectionsList] = useState(false); - const [showEndpointList, setShowEndpointList] = useState(false); + const [showDetectionsListsOnly, setShowDetectionsList] = useState(false); + const [showEndpointListsOnly, setShowEndpointList] = useState(false); const [isAddExceptionMenuOpen, setAddExceptionMenuOpen] = useState(false); useEffect((): void => { onFilterChange({ - filter: { filter, showDetectionsList, showEndpointList, tags }, - pagination: {}, + filter: { filter, tags }, + pagination: { + pageIndex: 0, + }, + showDetectionsListsOnly, + showEndpointListsOnly, }); - }, [filter, tags, showDetectionsList, showEndpointList, onFilterChange]); + }, [filter, tags, showDetectionsListsOnly, showEndpointListsOnly, onFilterChange]); const onAddExceptionDropdownClick = useCallback( (): void => setAddExceptionMenuOpen(!isAddExceptionMenuOpen), @@ -60,14 +64,14 @@ const ExceptionsViewerHeaderComponent = ({ ); const handleDetectionsListClick = useCallback((): void => { - setShowDetectionsList(!showDetectionsList); + setShowDetectionsList(!showDetectionsListsOnly); setShowEndpointList(false); - }, [showDetectionsList, setShowDetectionsList, setShowEndpointList]); + }, [showDetectionsListsOnly, setShowDetectionsList, setShowEndpointList]); const handleEndpointListClick = useCallback((): void => { - setShowEndpointList(!showEndpointList); + setShowEndpointList(!showEndpointListsOnly); setShowDetectionsList(false); - }, [showEndpointList, setShowEndpointList, setShowDetectionsList]); + }, [showEndpointListsOnly, setShowEndpointList, setShowDetectionsList]); const handleOnSearch = useCallback( (searchValue: string): void => { @@ -148,7 +152,7 @@ const ExceptionsViewerHeaderComponent = ({ @@ -157,7 +161,7 @@ const ExceptionsViewerHeaderComponent = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx similarity index 78% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_item.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx index 7ccb8d251eae10..3024ae0f141448 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx @@ -9,6 +9,7 @@ import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import * as i18n from '../translations'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; @@ -17,7 +18,8 @@ describe('ExceptionsViewerItems', () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptTitle"]').text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_TITLE + ); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_BODY + ); + }); + + it('it renders no search results found prompt if "showNoResults" is "true"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptTitle"]').text()).toEqual(''); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').text()).toEqual( + i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY + ); }); it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => { @@ -37,6 +69,7 @@ describe('ExceptionsViewerItems', () => { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> = ({ showEmpty, + showNoResults, isInitLoading, exceptions, loadingItemIds, @@ -51,12 +53,22 @@ const ExceptionsViewerItemsComponent: React.FC = ({ onEditExceptionItem, }): JSX.Element => ( - {showEmpty || isInitLoading ? ( + {showEmpty || showNoResults || isInitLoading ? ( {i18n.EXCEPTION_EMPTY_PROMPT_TITLE}} - body={

{i18n.EXCEPTION_EMPTY_PROMPT_BODY}

} + iconType={showNoResults ? 'searchProfilerApp' : 'list'} + title={ +

+ {showNoResults ? '' : i18n.EXCEPTION_EMPTY_PROMPT_TITLE} +

+ } + body={ +

+ {showNoResults + ? i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY + : i18n.EXCEPTION_EMPTY_PROMPT_BODY} +

+ } data-test-subj="exceptionsEmptyPrompt" />
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 7f4d8252764060..7482068454a97c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useEffect, useReducer } from 'react'; +import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiSpacer } from '@elastic/eui'; import uuid from 'uuid'; @@ -31,23 +31,23 @@ import { EditExceptionModal } from '../edit_exception_modal'; import { AddExceptionModal } from '../add_exception_modal'; const initialState: State = { - filterOptions: { filter: '', showEndpointList: false, showDetectionsList: false, tags: [] }, + filterOptions: { filter: '', tags: [] }, pagination: { pageIndex: 0, pageSize: 20, totalItemCount: 0, pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], }, - endpointList: null, - detectionsList: null, - allExceptions: [], exceptions: [], exceptionToEdit: null, - loadingLists: [], loadingItemIds: [], isInitLoading: true, currentModal: null, exceptionListTypeToEdit: null, + totalEndpointItems: 0, + totalDetectionsItems: 0, + showEndpointListsOnly: false, + showDetectionsListsOnly: false, }; interface ExceptionsViewerProps { @@ -87,46 +87,47 @@ const ExceptionsViewerComponent = ({ ); const [ { - endpointList, - detectionsList, exceptions, filterOptions, pagination, - loadingLists, loadingItemIds, isInitLoading, currentModal, exceptionToEdit, exceptionListTypeToEdit, + totalEndpointItems, + totalDetectionsItems, + showDetectionsListsOnly, + showEndpointListsOnly, }, dispatch, - ] = useReducer(allExceptionItemsReducer(), { ...initialState, loadingLists: exceptionListsMeta }); - const { deleteExceptionItem } = useApi(services.http); + ] = useReducer(allExceptionItemsReducer(), { ...initialState }); + const { deleteExceptionItem, getExceptionListsItems } = useApi(services.http); const setExceptions = useCallback( - ({ - lists: newLists, - exceptions: newExceptions, - pagination: newPagination, - }: UseExceptionListSuccess): void => { + ({ exceptions: newExceptions, pagination: newPagination }: UseExceptionListSuccess): void => { dispatch({ type: 'setExceptions', - lists: newLists, + lists: exceptionListsMeta, exceptions: newExceptions, pagination: newPagination, }); }, - [dispatch] + [dispatch, exceptionListsMeta] ); - const [loadingList, , , , fetchList] = useExceptionList({ + const [loadingList, , , fetchListItems] = useExceptionList({ http: services.http, - lists: loadingLists, - filterOptions, + lists: exceptionListsMeta, + filterOptions: + filterOptions.filter !== '' || filterOptions.tags.length > 0 ? [filterOptions] : [], pagination: { page: pagination.pageIndex + 1, perPage: pagination.pageSize, total: pagination.totalItemCount, }, + showDetectionsListsOnly, + showEndpointListsOnly, + matchFilters: true, onSuccess: setExceptions, onError: onDispatchToaster({ color: 'danger', @@ -145,22 +146,81 @@ const ExceptionsViewerComponent = ({ [dispatch] ); + const setExceptionItemTotals = useCallback( + (endpointItemTotals: number | null, detectionItemTotals: number | null): void => { + dispatch({ + type: 'setExceptionItemTotals', + totalEndpointItems: endpointItemTotals, + totalDetectionsItems: detectionItemTotals, + }); + }, + [dispatch] + ); + + const handleGetTotals = useCallback(async (): Promise => { + await getExceptionListsItems({ + lists: exceptionListsMeta, + filterOptions: [], + pagination: { + page: 0, + perPage: 1, + total: 0, + }, + showDetectionsListsOnly: true, + showEndpointListsOnly: false, + onSuccess: ({ pagination: detectionPagination }) => { + setExceptionItemTotals(null, detectionPagination.total ?? 0); + }, + onError: () => { + const dispatchToasterError = onDispatchToaster({ + color: 'danger', + title: i18n.TOTAL_ITEMS_FETCH_ERROR, + iconType: 'alert', + }); + + dispatchToasterError(); + }, + }); + await getExceptionListsItems({ + lists: exceptionListsMeta, + filterOptions: [], + pagination: { + page: 0, + perPage: 1, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, + onSuccess: ({ pagination: endpointPagination }) => { + setExceptionItemTotals(endpointPagination.total ?? 0, null); + }, + onError: () => { + const dispatchToasterError = onDispatchToaster({ + color: 'danger', + title: i18n.TOTAL_ITEMS_FETCH_ERROR, + iconType: 'alert', + }); + + dispatchToasterError(); + }, + }); + }, [setExceptionItemTotals, exceptionListsMeta, getExceptionListsItems, onDispatchToaster]); + const handleFetchList = useCallback((): void => { - if (fetchList != null) { - fetchList(); + if (fetchListItems != null) { + fetchListItems(); + handleGetTotals(); } - }, [fetchList]); + }, [fetchListItems, handleGetTotals]); const handleFilterChange = useCallback( - ({ filter, pagination: pag }: Filter): void => { + (filters: Partial): void => { dispatch({ type: 'updateFilterOptions', - filterOptions: filter, - pagination: pag, - allLists: exceptionListsMeta, + filters, }); }, - [dispatch, exceptionListsMeta] + [dispatch] ); const handleAddException = useCallback( @@ -178,12 +238,13 @@ const ExceptionsViewerComponent = ({ (exception: ExceptionListItemSchema): void => { dispatch({ type: 'updateExceptionToEdit', + lists: exceptionListsMeta, exception, }); setCurrentModal('editModal'); }, - [setCurrentModal] + [setCurrentModal, exceptionListsMeta] ); const handleOnCancelExceptionModal = useCallback((): void => { @@ -235,23 +296,24 @@ const ExceptionsViewerComponent = ({ // Logic for initial render useEffect((): void => { if (isInitLoading && !loadingList && (exceptions.length === 0 || exceptions != null)) { + handleGetTotals(); dispatch({ type: 'updateIsInitLoading', loading: false, }); } - }, [isInitLoading, exceptions, loadingList, dispatch]); + }, [handleGetTotals, isInitLoading, exceptions, loadingList, dispatch]); // Used in utility bar info text - const ruleSettingsUrl = useMemo((): string => { - return services.application.getUrlForApp( - `security/detections/rules/id/${encodeURI(ruleId)}/edit` - ); - }, [ruleId, services.application]); + const ruleSettingsUrl = services.application.getUrlForApp( + `security/detections/rules/id/${encodeURI(ruleId)}/edit` + ); + + const showEmpty: boolean = + !isInitLoading && !loadingList && totalEndpointItems === 0 && totalDetectionsItems === 0; - const showEmpty = useMemo((): boolean => { - return !isInitLoading && !loadingList && exceptions.length === 0; - }, [isInitLoading, exceptions.length, loadingList]); + const showNoResults: boolean = + exceptions.length === 0 && (totalEndpointItems > 0 || totalDetectionsItems > 0); return ( <> @@ -288,8 +350,8 @@ const ExceptionsViewerComponent = ({ @@ -298,13 +360,15 @@ const ExceptionsViewerComponent = ({ ; - pagination: Partial; - allLists: ExceptionIdentifiers[]; + filters: Partial; } | { type: 'updateIsInitLoading'; loading: boolean } | { type: 'updateModalOpen'; modalName: ViewerModalName } - | { type: 'updateExceptionToEdit'; exception: ExceptionListItemSchema } + | { + type: 'updateExceptionToEdit'; + lists: ExceptionIdentifiers[]; + exception: ExceptionListItemSchema; + } | { type: 'updateLoadingItemIds'; items: ExceptionListItemIdentifiers[] } - | { type: 'updateExceptionListTypeToEdit'; exceptionListType: ExceptionListType | null }; + | { type: 'updateExceptionListTypeToEdit'; exceptionListType: ExceptionListType | null } + | { + type: 'setExceptionItemTotals'; + totalEndpointItems: number | null; + totalDetectionsItems: number | null; + }; export const allExceptionItemsReducer = () => (state: State, action: Action): State => { switch (action.type) { case 'setExceptions': { - const endpointList = action.lists.filter((t) => t.type === 'endpoint'); - const detectionsList = action.lists.filter((t) => t.type === 'detection'); + const { exceptions, pagination } = action; return { ...state, - endpointList: state.filterOptions.showDetectionsList - ? state.endpointList - : endpointList[0] ?? null, - detectionsList: state.filterOptions.showEndpointList - ? state.detectionsList - : detectionsList[0] ?? null, pagination: { ...state.pagination, - pageIndex: action.pagination.page - 1, - pageSize: action.pagination.perPage, - totalItemCount: action.pagination.total ?? 0, + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total ?? 0, }, - allExceptions: action.exceptions, - exceptions: action.exceptions, + exceptions, }; } case 'updateFilterOptions': { - const returnState = { + const { filter, pagination, showEndpointListsOnly, showDetectionsListsOnly } = action.filters; + return { ...state, filterOptions: { ...state.filterOptions, - ...action.filterOptions, + ...filter, }, pagination: { ...state.pagination, - ...action.pagination, + ...pagination, }, + showEndpointListsOnly: showEndpointListsOnly ?? state.showEndpointListsOnly, + showDetectionsListsOnly: showDetectionsListsOnly ?? state.showDetectionsListsOnly, + }; + } + case 'setExceptionItemTotals': { + return { + ...state, + totalEndpointItems: + action.totalEndpointItems == null ? state.totalEndpointItems : action.totalEndpointItems, + totalDetectionsItems: + action.totalDetectionsItems == null + ? state.totalDetectionsItems + : action.totalDetectionsItems, }; - - if (action.filterOptions.showEndpointList) { - const list = action.allLists.filter((t) => t.type === 'endpoint'); - - return { - ...returnState, - loadingLists: list, - exceptions: list.length === 0 ? [] : [...state.exceptions], - }; - } else if (action.filterOptions.showDetectionsList) { - const list = action.allLists.filter((t) => t.type === 'detection'); - - return { - ...returnState, - loadingLists: list, - exceptions: list.length === 0 ? [] : [...state.exceptions], - }; - } else { - return { - ...returnState, - loadingLists: action.allLists, - }; - } } case 'updateIsInitLoading': { return { @@ -121,13 +115,13 @@ export const allExceptionItemsReducer = () => (state: State, action: Action): St }; } case 'updateExceptionToEdit': { - const exception = action.exception; - const exceptionListToEdit = [state.endpointList, state.detectionsList].find((list) => { - return list !== null && exception.list_id === list.list_id; + const { exception, lists } = action; + const exceptionListToEdit = lists.find((list) => { + return list !== null && exception.list_id === list.listId; }); return { ...state, - exceptionToEdit: action.exception, + exceptionToEdit: exception, exceptionListTypeToEdit: exceptionListToEdit ? exceptionListToEdit.type : null, }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json index 56c9f151dc7129..bec88bcb0e30e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json @@ -1,4 +1,60 @@ { - "rule_id": "query-rule-id", - "name": "Changes only the name to this new value" + "author": [], + "actions": [], + "description": "endpoint list only", + "enabled": true, + "false_positives": [], + "filters": [], + "from": "now-360s", + "index": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "interval": "5m", + "rule_id": "bf8ee47a-3f7f-4561-b2e6-92c9d618a0b2", + "language": "kuery", + "license": "", + "output_index": ".siem-signals-ytercero-default", + "max_signals": 100, + "risk_score": 50, + "risk_score_mapping": [], + "name": "endpoint list only", + "query": "host.name: * ", + "references": [], + "meta": { + "from": "1m", + "kibana_siem_app_url": "http://localhost:5601/app/security" + }, + "severity": "low", + "severity_mapping": [], + "tags": [], + "to": "now", + "type": "query", + "threat": [], + "throttle": "no_actions", + "exceptions_list": [ + { + "list_id": "endpoint_list", + "namespace_type": "agnostic", + "id": "endpoint_list", + "type": "endpoint" + }, + { + "list_id": "b27b7e13-4105-49cf-8142-cee0c61de321", + "namespace_type": "single", + "id": "8da260a0-d1bb-11ea-b248-4ba44bc54af7", + "type": "detection" + }, + { + "list_id": "b27b7e13-4105-49cf-8142-cee0c61de321", + "namespace_type": "single", + "id": "8da260a0-d1bb-11ea-b248-4ba44bc54af7", + "type": "detection" + } + ] } From 3d08276549c1c7e510de69505c147c6cc908e26e Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 29 Jul 2020 17:07:15 -0700 Subject: [PATCH 6/8] skip flaky suite (#69783) (#70043) --- .../public/test/client_integration/job_create_review.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js index 3dbbe70bfc5603..87fdabae182402 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js @@ -25,7 +25,9 @@ jest.mock('../../kibana_services', () => { const { setup } = pageHelpers.jobCreate; -describe('Create Rollup Job, step 6: Review', () => { +// FLAKY: https://github.com/elastic/kibana/issues/69783 +// FLAKY: https://github.com/elastic/kibana/issues/70043 +describe.skip('Create Rollup Job, step 6: Review', () => { let find; let exists; let actions; From 86c13869a2cd780bf21ce9ce8ca724ce3c7911e5 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 29 Jul 2020 17:47:01 -0700 Subject: [PATCH 7/8] Fix Snapshot Restore /policies/indices API endpoint on Cloud (#73734) * Add leading slash to transport.request path. * Remove unnecessary 'hidden' expand_wildcards option, since 'all' includes hidden indices. --- x-pack/plugins/snapshot_restore/server/routes/api/policy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index b8e70125295544..688a1a409680a2 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -236,9 +236,9 @@ export function registerPolicyRoutes({ 'transport.request', { method: 'GET', - path: `_resolve/index/*`, + path: `/_resolve/index/*`, query: { - expand_wildcards: 'all,hidden', + expand_wildcards: 'all', }, } ); From 6bfd4e6af34360b621f073804f7c6cc3884a3ec4 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 29 Jul 2020 17:53:55 -0700 Subject: [PATCH 8/8] [build] rewrite source as transpiled JS later in the process (#73749) Co-authored-by: spalger --- .../kbn-optimizer/src/worker/run_compilers.ts | 15 ++++++++++----- .../kbn-optimizer/src/worker/webpack_helpers.ts | 1 + src/dev/build/build_distributables.ts | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index c7be943d65a489..d78eb8214f607f 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -110,7 +110,7 @@ const observeCompiler = ( const bundleRefExportIds: string[] = []; const referencedFiles = new Set(); - let normalModuleCount = 0; + let moduleCount = 0; let workUnits = stats.compilation.fileDependencies.size; if (bundle.manifestPath) { @@ -119,7 +119,7 @@ const observeCompiler = ( for (const module of stats.compilation.modules) { if (isNormalModule(module)) { - normalModuleCount += 1; + moduleCount += 1; const path = getModulePath(module); const parsedPath = parseFilePath(path); @@ -154,7 +154,12 @@ const observeCompiler = ( continue; } - if (isExternalModule(module) || isIgnoredModule(module) || isConcatenatedModule(module)) { + if (isConcatenatedModule(module)) { + moduleCount += module.modules.length; + continue; + } + + if (isExternalModule(module) || isIgnoredModule(module)) { continue; } @@ -180,13 +185,13 @@ const observeCompiler = ( bundleRefExportIds, optimizerCacheKey: workerConfig.optimizerCacheKey, cacheKey: bundle.createCacheKey(files, mtimes), - moduleCount: normalModuleCount, + moduleCount, workUnits, files, }); return compilerMsgs.compilerSuccess({ - moduleCount: normalModuleCount, + moduleCount, }); }) ); diff --git a/packages/kbn-optimizer/src/worker/webpack_helpers.ts b/packages/kbn-optimizer/src/worker/webpack_helpers.ts index e30920b9601444..a1f97c4314774b 100644 --- a/packages/kbn-optimizer/src/worker/webpack_helpers.ts +++ b/packages/kbn-optimizer/src/worker/webpack_helpers.ts @@ -155,6 +155,7 @@ export interface WebpackConcatenatedModule { id: number; dependencies: Dependency[]; usedExports: string[]; + modules: unknown[]; } export function isConcatenatedModule(module: any): module is WebpackConcatenatedModule { diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index bfcc98d6cd9a87..1d41f4c270caab 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -63,17 +63,17 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions await run(Tasks.CopyBinScripts); await run(Tasks.CreateEmptyDirsAndFiles); await run(Tasks.CreateReadme); - await run(Tasks.TranspileBabel); await run(Tasks.BuildPackages); await run(Tasks.CreatePackageJson); await run(Tasks.InstallDependencies); + await run(Tasks.BuildKibanaPlatformPlugins); + await run(Tasks.TranspileBabel); await run(Tasks.RemoveWorkspaces); await run(Tasks.CleanPackages); await run(Tasks.CreateNoticeFile); await run(Tasks.UpdateLicenseFile); await run(Tasks.RemovePackageJsonDeps); await run(Tasks.TranspileScss); - await run(Tasks.BuildKibanaPlatformPlugins); await run(Tasks.OptimizeBuild); await run(Tasks.CleanTypescript); await run(Tasks.CleanExtraFilesFromModules);