mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Cloud Security] Deleting K8S Dashboard (#207127)
## Summary As K8S Dashboard is currently hidden on main , the code serves no purpose other than potentially causing Tech debts whenever a refactor or a migration happens. As such its better to remove it completely. In case we want to bring it back later we will just pull it from git history > [!CAUTION] > **This should only affect Serverless and Main, 8.x.x should still be able to see and access K8S Dashboard** ## Related Tickets - https://github.com/elastic/security-team/issues/11418 - https://github.com/elastic/security-team/issues/10735 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Paulo Silva <paulo.henrique@elastic.co> Co-authored-by: Maxim Kholod <maxim.kholod@elastic.co>
This commit is contained in:
parent
5e47da357d
commit
a2d36067e9
122 changed files with 3 additions and 7293 deletions
|
@ -286,7 +286,6 @@ enabled:
|
|||
- x-pack/test/functional_cloud/saml.config.ts
|
||||
- x-pack/test/functional_solution_sidenav/config.ts
|
||||
- x-pack/test/functional_search/config.ts
|
||||
- x-pack/test/kubernetes_security/basic/config.ts
|
||||
- x-pack/test/licensing_plugin/config.public.ts
|
||||
- x-pack/test/licensing_plugin/config.ts
|
||||
- x-pack/test/monitoring_api_integration/config.ts
|
||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -998,7 +998,6 @@ x-pack/solutions/security/plugins/cloud_defend @elastic/kibana-cloud-security-po
|
|||
x-pack/solutions/security/plugins/cloud_security_posture @elastic/kibana-cloud-security-posture
|
||||
x-pack/solutions/security/plugins/ecs_data_quality_dashboard @elastic/security-threat-hunting-explore
|
||||
x-pack/solutions/security/plugins/elastic_assistant @elastic/security-generative-ai
|
||||
x-pack/solutions/security/plugins/kubernetes_security @elastic/kibana-cloud-security-posture
|
||||
x-pack/solutions/security/plugins/lists @elastic/security-detection-engine
|
||||
x-pack/solutions/security/plugins/security_solution @elastic/security-solution
|
||||
x-pack/solutions/security/plugins/security_solution_ess @elastic/security-solution
|
||||
|
|
|
@ -670,10 +670,6 @@ the infrastructure monitoring use-case within Kibana.
|
|||
|undefined
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/solutions/security/plugins/kubernetes_security/README.md[kubernetesSecurity]
|
||||
|This plugin provides interactive visualizations of your Kubernetes workload and session data.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/platform/plugins/shared/lens/readme.md[lens]
|
||||
|Lens is a visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads.
|
||||
|
||||
|
|
|
@ -605,7 +605,6 @@
|
|||
"@kbn/kibana-react-plugin": "link:src/platform/plugins/shared/kibana_react",
|
||||
"@kbn/kibana-usage-collection-plugin": "link:src/platform/plugins/private/kibana_usage_collection",
|
||||
"@kbn/kibana-utils-plugin": "link:src/platform/plugins/shared/kibana_utils",
|
||||
"@kbn/kubernetes-security-plugin": "link:x-pack/solutions/security/plugins/kubernetes_security",
|
||||
"@kbn/langchain": "link:x-pack/platform/packages/shared/kbn-langchain",
|
||||
"@kbn/language-documentation": "link:src/platform/packages/private/kbn-language-documentation",
|
||||
"@kbn/lens-config-builder-example-plugin": "link:x-pack/examples/lens_config_builder_example",
|
||||
|
|
|
@ -93,7 +93,6 @@ pageLoadAssetSize:
|
|||
kibanaReact: 74422
|
||||
kibanaUsageCollection: 16463
|
||||
kibanaUtils: 79713
|
||||
kubernetesSecurity: 77234
|
||||
lens: 57135
|
||||
licenseManagement: 41817
|
||||
licensing: 29004
|
||||
|
|
|
@ -1136,8 +1136,6 @@
|
|||
"@kbn/kibana-usage-collection-plugin/*": ["src/platform/plugins/private/kibana_usage_collection/*"],
|
||||
"@kbn/kibana-utils-plugin": ["src/platform/plugins/shared/kibana_utils"],
|
||||
"@kbn/kibana-utils-plugin/*": ["src/platform/plugins/shared/kibana_utils/*"],
|
||||
"@kbn/kubernetes-security-plugin": ["x-pack/solutions/security/plugins/kubernetes_security"],
|
||||
"@kbn/kubernetes-security-plugin/*": ["x-pack/solutions/security/plugins/kubernetes_security/*"],
|
||||
"@kbn/langchain": ["x-pack/platform/packages/shared/kbn-langchain"],
|
||||
"@kbn/langchain/*": ["x-pack/platform/packages/shared/kbn-langchain/*"],
|
||||
"@kbn/language-documentation": ["src/platform/packages/private/kbn-language-documentation"],
|
||||
|
|
|
@ -14043,7 +14043,6 @@
|
|||
"xpack.cloudDefend.controlSelectorsHelp": "Créez des sélecteurs de fichier ou de processus pour trouver une correspondance sur des opérations et/ou des conditions d'intérêt.",
|
||||
"xpack.cloudDefend.controlYamlHelp": "Configurez votre stratégie en créant des sélecteurs de \"fichier\" ou \"processus\" et des réponses ci-dessous.",
|
||||
"xpack.cloudDefend.controlYamlView": "Vue YAML",
|
||||
"xpack.cloudDefend.createPackagePolicy.customAssetsTab.dashboardViewLabel": "Voir les tableaux de bord k8s",
|
||||
"xpack.cloudDefend.description": "Description",
|
||||
"xpack.cloudDefend.enableControl": "Activer la politique",
|
||||
"xpack.cloudDefend.enableControlHelp": "Active la politique de prévention des dérives, d’alerte et de logging montrée ci-dessous.",
|
||||
|
@ -24031,48 +24030,6 @@
|
|||
"xpack.investigateApp.useUpdateInvestigationNote.errorMessage": "une erreur s'est produite",
|
||||
"xpack.investigateApp.useUpdateInvestigationNote.errorTitle": "Erreur",
|
||||
"xpack.investigateApp.useUpdateInvestigationNote.successMessage": "Note mise à jour",
|
||||
"xpack.kubernetesSecurity.chartsToggle.hide": "Masquer les graphiques",
|
||||
"xpack.kubernetesSecurity.chartsToggle.show": "Afficher les graphiques",
|
||||
"xpack.kubernetesSecurity.containerNameWidget.containerImage": "Image du conteneur",
|
||||
"xpack.kubernetesSecurity.containerNameWidget.containerImageAriaLabel": "Widget de session de nom de conteneur",
|
||||
"xpack.kubernetesSecurity.containerNameWidget.containerImageCountColumn": "Nombre de sessions",
|
||||
"xpack.kubernetesSecurity.countWidget.clusters": "Clusters",
|
||||
"xpack.kubernetesSecurity.countWidget.containerImages": "Images de conteneurs",
|
||||
"xpack.kubernetesSecurity.countWidget.namespace": "Espace de nom",
|
||||
"xpack.kubernetesSecurity.countWidget.nodes": "Nœuds",
|
||||
"xpack.kubernetesSecurity.countWidget.pods": "Pods",
|
||||
"xpack.kubernetesSecurity.entryUserChart.nonRoot": "Non root",
|
||||
"xpack.kubernetesSecurity.entryUserChart.root": "Root",
|
||||
"xpack.kubernetesSecurity.entryUserChart.title": "Utilisateurs de session d’entrée",
|
||||
"xpack.kubernetesSecurity.entryUserChart.tooltip": "L'utilisateur de la session d’entrée est l'utilisateur Linux initial associé à la session. Cet utilisateur peut être défini à partir de l'authentification d'un login distant ou automatiquement pour les sessions de service démarrées par init.",
|
||||
"xpack.kubernetesSecurity.searchGroup.cluster": "Cluster",
|
||||
"xpack.kubernetesSecurity.searchGroup.groupBy": "Regrouper par",
|
||||
"xpack.kubernetesSecurity.searchGroup.sortBy": "Trier par",
|
||||
"xpack.kubernetesSecurity.sessionChart.interactive": "Interactif",
|
||||
"xpack.kubernetesSecurity.sessionChart.nonInteractive": "Non interactif",
|
||||
"xpack.kubernetesSecurity.sessionChart.title": "Interactivité des sessions",
|
||||
"xpack.kubernetesSecurity.sessionChart.tooltip": "Les sessions interactives disposent d'un terminal de contrôle et impliquent souvent qu'un humain entre les commandes.",
|
||||
"xpack.kubernetesSecurity.treeNav.cluster": "{isPlural, select, true {clusters} other {cluster}}",
|
||||
"xpack.kubernetesSecurity.treeNav.containerImage": "{isPlural, select, true {images de conteneurs} other {image de conteneur}}",
|
||||
"xpack.kubernetesSecurity.treeNav.namespace": "{isPlural, select, true {espaces de noms} other {espace de nom}}",
|
||||
"xpack.kubernetesSecurity.treeNav.node": "{isPlural, select, true {nœuds} other {nœud}}",
|
||||
"xpack.kubernetesSecurity.treeNav.pod": "{isPlural, select, true {pods} other {pod}}",
|
||||
"xpack.kubernetesSecurity.treeNavigation.collapse": "Réduire la navigation arborescente",
|
||||
"xpack.kubernetesSecurity.treeNavigation.empty": "Aucune donnée disponible",
|
||||
"xpack.kubernetesSecurity.treeNavigation.expand": "Développer la navigation arborescente",
|
||||
"xpack.kubernetesSecurity.treeNavigation.loading": "Chargement",
|
||||
"xpack.kubernetesSecurity.treeNavigation.loadMore": "Afficher plus de {name}",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.clusterId": "Cluster",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.clusterName": "Cluster",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.containerImage": "Image du conteneur",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.namespace": "Espace de nom",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.node": "Nœud",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.pod": "Pod",
|
||||
"xpack.kubernetesSecurity.treeView.empty.description": "Essayer de rechercher sur une période plus longue ou de modifier votre recherche",
|
||||
"xpack.kubernetesSecurity.treeView.empty.title": "Aucun résultat ne correspond à vos critères de recherche.",
|
||||
"xpack.kubernetesSecurity.treeView.infrastructureView": "Vue d’infrastructure",
|
||||
"xpack.kubernetesSecurity.treeView.logicalView": "Vue logique",
|
||||
"xpack.kubernetesSecurity.treeView.switherLegend": "Vous pouvez basculer entre la vue logique et la vue d’infrastructure",
|
||||
"xpack.lens.action.exploreInDiscover": "Explorer dans Discover",
|
||||
"xpack.lens.AggBasedLabel": "visualisation basée sur l'agrégation",
|
||||
"xpack.lens.app.addToLibrary": "Enregistrer dans la bibliothèque",
|
||||
|
@ -34746,7 +34703,6 @@
|
|||
"xpack.securitySolution.appLinks.hosts.risk": "Risque de l'hôte",
|
||||
"xpack.securitySolution.appLinks.hosts.sessions": "Sessions",
|
||||
"xpack.securitySolution.appLinks.hosts.uncommonProcesses": "Processus inhabituels",
|
||||
"xpack.securitySolution.appLinks.kubernetesDescription": "Fournit des visualisations interactives de votre charge de travail et de vos données de session Kubernetes.",
|
||||
"xpack.securitySolution.appLinks.manage": "Gérer",
|
||||
"xpack.securitySolution.appLinks.ml.keyword": "Machine Learning",
|
||||
"xpack.securitySolution.appLinks.ml.title": "Machine Learning",
|
||||
|
@ -38946,13 +38902,6 @@
|
|||
"xpack.securitySolution.kpiNetwork.uniquePrivateIps.title": "IP privés uniques",
|
||||
"xpack.securitySolution.kpiUsers.totalUsers.errorSearchDescription": "Une erreur s'est produite sur la recherche du KPI du total des utilisateurs",
|
||||
"xpack.securitySolution.kpiUsers.totalUsers.title": "Utilisateurs",
|
||||
"xpack.securitySolution.kubernetes.columnContainer": "Conteneur",
|
||||
"xpack.securitySolution.kubernetes.columnEntryUser": "ID utilisateur",
|
||||
"xpack.securitySolution.kubernetes.columnExecutable": "Exécutable",
|
||||
"xpack.securitySolution.kubernetes.columnInteractive": "Interactif",
|
||||
"xpack.securitySolution.kubernetes.columnNode": "Nœud",
|
||||
"xpack.securitySolution.kubernetes.columnPod": "Pod",
|
||||
"xpack.securitySolution.kubernetes.columnSessionStart": "Date de démarrage",
|
||||
"xpack.securitySolution.landing.threatHunting.hostsDescription": "Aperçu complet de tous les hôtes et événements de sécurité des hôtes.",
|
||||
"xpack.securitySolution.lastEventTime.failSearchDescription": "Impossible de lancer une recherche sur la dernière heure de l'événement",
|
||||
"xpack.securitySolution.lensEmbeddable.NoDataToDisplay.title": "Aucune donnée à afficher",
|
||||
|
|
|
@ -13913,7 +13913,6 @@
|
|||
"xpack.cloudDefend.controlSelectorsHelp": "ファイルまたはプロセスセレクターを作成し、関心がある処理または条件で照合します。",
|
||||
"xpack.cloudDefend.controlYamlHelp": "「ファイル」または「プロセス」セレクターと次の対応を作成して、ポリシーを構成します。",
|
||||
"xpack.cloudDefend.controlYamlView": "YAMLビュー",
|
||||
"xpack.cloudDefend.createPackagePolicy.customAssetsTab.dashboardViewLabel": "k8sダッシュボードを表示",
|
||||
"xpack.cloudDefend.description": "説明",
|
||||
"xpack.cloudDefend.enableControl": "ポリシーを有効にする",
|
||||
"xpack.cloudDefend.enableControlHelp": "以下に示すドリフト防止、アラート、ログポリシーを有効にします。",
|
||||
|
@ -23891,48 +23890,6 @@
|
|||
"xpack.investigateApp.useUpdateInvestigationNote.errorMessage": "エラーが発生しました",
|
||||
"xpack.investigateApp.useUpdateInvestigationNote.errorTitle": "エラー",
|
||||
"xpack.investigateApp.useUpdateInvestigationNote.successMessage": "メモが更新されました",
|
||||
"xpack.kubernetesSecurity.chartsToggle.hide": "グラフを非表示",
|
||||
"xpack.kubernetesSecurity.chartsToggle.show": "チャートを表示",
|
||||
"xpack.kubernetesSecurity.containerNameWidget.containerImage": "コンテナーイメージ",
|
||||
"xpack.kubernetesSecurity.containerNameWidget.containerImageAriaLabel": "コンテナー名セッションウィジェット",
|
||||
"xpack.kubernetesSecurity.containerNameWidget.containerImageCountColumn": "セッションカウント",
|
||||
"xpack.kubernetesSecurity.countWidget.clusters": "クラスター",
|
||||
"xpack.kubernetesSecurity.countWidget.containerImages": "コンテナーイメージ",
|
||||
"xpack.kubernetesSecurity.countWidget.namespace": "名前空間",
|
||||
"xpack.kubernetesSecurity.countWidget.nodes": "ノード",
|
||||
"xpack.kubernetesSecurity.countWidget.pods": "ポッド",
|
||||
"xpack.kubernetesSecurity.entryUserChart.nonRoot": "非ルート",
|
||||
"xpack.kubernetesSecurity.entryUserChart.root": "ルート",
|
||||
"xpack.kubernetesSecurity.entryUserChart.title": "エントリセッションユーザー",
|
||||
"xpack.kubernetesSecurity.entryUserChart.tooltip": "エントリセッションユーザーは、セッションに関連付けられた最初のLinuxユーザーです。このユーザーは、リモートログインの認証から設定されるか、initで開始されたサービスセッションでは自動的に設定される場合があります。",
|
||||
"xpack.kubernetesSecurity.searchGroup.cluster": "クラスター",
|
||||
"xpack.kubernetesSecurity.searchGroup.groupBy": "グループ分けの条件",
|
||||
"xpack.kubernetesSecurity.searchGroup.sortBy": "並べ替え基準",
|
||||
"xpack.kubernetesSecurity.sessionChart.interactive": "インタラクティブ",
|
||||
"xpack.kubernetesSecurity.sessionChart.nonInteractive": "非インタラクティブ",
|
||||
"xpack.kubernetesSecurity.sessionChart.title": "セッションのインタラクティブ",
|
||||
"xpack.kubernetesSecurity.sessionChart.tooltip": "インタラクティブなセッションには、制御ターミナルがあり、多くの場合、人間がコマンドを入力しています。",
|
||||
"xpack.kubernetesSecurity.treeNav.cluster": "{isPlural, select, true {クラスター} other {クラスター}}",
|
||||
"xpack.kubernetesSecurity.treeNav.containerImage": "{isPlural, select, true {コンテナーイメージ} other { コンテナーイメージ}}",
|
||||
"xpack.kubernetesSecurity.treeNav.namespace": "{isPlural, select, true {名前空間} other {名前空間}}",
|
||||
"xpack.kubernetesSecurity.treeNav.node": "{isPlural, select, true {ノード} other {ノード}}",
|
||||
"xpack.kubernetesSecurity.treeNav.pod": "{isPlural, select, true {ポッド} other {ポッド}}",
|
||||
"xpack.kubernetesSecurity.treeNavigation.collapse": "ツリーナビゲーションを折りたたむ",
|
||||
"xpack.kubernetesSecurity.treeNavigation.empty": "利用可能なデータがありません",
|
||||
"xpack.kubernetesSecurity.treeNavigation.expand": "ツリーナビゲーションを展開",
|
||||
"xpack.kubernetesSecurity.treeNavigation.loading": "読み込み中",
|
||||
"xpack.kubernetesSecurity.treeNavigation.loadMore": "その他の{name}を表示",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.clusterId": "クラスター",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.clusterName": "クラスター",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.containerImage": "コンテナーイメージ",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.namespace": "名前空間",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.node": "ノード",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.pod": "ポッド",
|
||||
"xpack.kubernetesSecurity.treeView.empty.description": "期間を長くして検索するか、検索を変更してください",
|
||||
"xpack.kubernetesSecurity.treeView.empty.title": "検索条件と一致する結果がありません。",
|
||||
"xpack.kubernetesSecurity.treeView.infrastructureView": "インフラストラクチャービュー",
|
||||
"xpack.kubernetesSecurity.treeView.logicalView": "論理ビュー",
|
||||
"xpack.kubernetesSecurity.treeView.switherLegend": "論理ビューとインフラストラクチャービューを切り替えることができます",
|
||||
"xpack.lens.action.exploreInDiscover": "Discoverで探索",
|
||||
"xpack.lens.AggBasedLabel": "集約に基づく可視化",
|
||||
"xpack.lens.app.addToLibrary": "ライブラリに保存",
|
||||
|
@ -34607,7 +34564,6 @@
|
|||
"xpack.securitySolution.appLinks.hosts.risk": "ホストリスク",
|
||||
"xpack.securitySolution.appLinks.hosts.sessions": "セッション",
|
||||
"xpack.securitySolution.appLinks.hosts.uncommonProcesses": "非共通プロセス",
|
||||
"xpack.securitySolution.appLinks.kubernetesDescription": "Kubernetesワークロードおよびセッションデータのインタラクティブビジュアライゼーションを提供します。",
|
||||
"xpack.securitySolution.appLinks.manage": "管理",
|
||||
"xpack.securitySolution.appLinks.ml.keyword": "機械学習",
|
||||
"xpack.securitySolution.appLinks.ml.title": "機械学習",
|
||||
|
@ -38806,13 +38762,6 @@
|
|||
"xpack.securitySolution.kpiNetwork.uniquePrivateIps.title": "固有のプライベート IP",
|
||||
"xpack.securitySolution.kpiUsers.totalUsers.errorSearchDescription": "統計ユーザーKPI検索でエラーが発生しました",
|
||||
"xpack.securitySolution.kpiUsers.totalUsers.title": "ユーザー",
|
||||
"xpack.securitySolution.kubernetes.columnContainer": "コンテナー",
|
||||
"xpack.securitySolution.kubernetes.columnEntryUser": "ユーザーID",
|
||||
"xpack.securitySolution.kubernetes.columnExecutable": "実行ファイル",
|
||||
"xpack.securitySolution.kubernetes.columnInteractive": "インタラクティブ",
|
||||
"xpack.securitySolution.kubernetes.columnNode": "ノード",
|
||||
"xpack.securitySolution.kubernetes.columnPod": "ポッド",
|
||||
"xpack.securitySolution.kubernetes.columnSessionStart": "開始日",
|
||||
"xpack.securitySolution.landing.threatHunting.hostsDescription": "すべてのホストとホスト関連のセキュリティイベントに関する包括的な概要。",
|
||||
"xpack.securitySolution.lastEventTime.failSearchDescription": "前回のイベント時刻で検索を実行できませんでした",
|
||||
"xpack.securitySolution.lensEmbeddable.NoDataToDisplay.title": "表示するデータがありません",
|
||||
|
|
|
@ -13677,7 +13677,6 @@
|
|||
"xpack.cloudDefend.controlSelectorsHelp": "创建文件或进程选择器以匹配相关操作和/或条件。",
|
||||
"xpack.cloudDefend.controlYamlHelp": "通过在下面创建'文件'或'进程'选择器和响应来配置您的策略。",
|
||||
"xpack.cloudDefend.controlYamlView": "YAML 视图",
|
||||
"xpack.cloudDefend.createPackagePolicy.customAssetsTab.dashboardViewLabel": "查看 k8s 仪表板",
|
||||
"xpack.cloudDefend.description": "描述",
|
||||
"xpack.cloudDefend.enableControl": "启用策略",
|
||||
"xpack.cloudDefend.enableControlHelp": "启用下面显示的偏移预防、告警和日志记录策略。",
|
||||
|
@ -23483,48 +23482,6 @@
|
|||
"xpack.investigateApp.useUpdateInvestigationNote.errorMessage": "发生错误",
|
||||
"xpack.investigateApp.useUpdateInvestigationNote.errorTitle": "错误",
|
||||
"xpack.investigateApp.useUpdateInvestigationNote.successMessage": "备注已更新",
|
||||
"xpack.kubernetesSecurity.chartsToggle.hide": "隐藏图表",
|
||||
"xpack.kubernetesSecurity.chartsToggle.show": "显示图表",
|
||||
"xpack.kubernetesSecurity.containerNameWidget.containerImage": "容器映像",
|
||||
"xpack.kubernetesSecurity.containerNameWidget.containerImageAriaLabel": "容器名称会话小组件",
|
||||
"xpack.kubernetesSecurity.containerNameWidget.containerImageCountColumn": "会话计数",
|
||||
"xpack.kubernetesSecurity.countWidget.clusters": "集群",
|
||||
"xpack.kubernetesSecurity.countWidget.containerImages": "容器映像",
|
||||
"xpack.kubernetesSecurity.countWidget.namespace": "命名空间",
|
||||
"xpack.kubernetesSecurity.countWidget.nodes": "节点",
|
||||
"xpack.kubernetesSecurity.countWidget.pods": "Pod",
|
||||
"xpack.kubernetesSecurity.entryUserChart.nonRoot": "非根",
|
||||
"xpack.kubernetesSecurity.entryUserChart.root": "根",
|
||||
"xpack.kubernetesSecurity.entryUserChart.title": "入口会话用户",
|
||||
"xpack.kubernetesSecurity.entryUserChart.tooltip": "入口会话用户是指与会话关联的初始 Linux 用户。可以在远程登录的身份验证期间设置此用户,或为由 init 启动的会话服务自动设置该用户。",
|
||||
"xpack.kubernetesSecurity.searchGroup.cluster": "集群",
|
||||
"xpack.kubernetesSecurity.searchGroup.groupBy": "分组依据",
|
||||
"xpack.kubernetesSecurity.searchGroup.sortBy": "排序依据",
|
||||
"xpack.kubernetesSecurity.sessionChart.interactive": "交互",
|
||||
"xpack.kubernetesSecurity.sessionChart.nonInteractive": "非交互",
|
||||
"xpack.kubernetesSecurity.sessionChart.title": "会话交互性",
|
||||
"xpack.kubernetesSecurity.sessionChart.tooltip": "交互式会话具有控制终端,通常表示人类正输入命令。",
|
||||
"xpack.kubernetesSecurity.treeNav.cluster": "{isPlural, select, other {集群}}",
|
||||
"xpack.kubernetesSecurity.treeNav.containerImage": "{isPlural, select, other {容器映像}}",
|
||||
"xpack.kubernetesSecurity.treeNav.namespace": "{isPlural, select, other {命名空间}}",
|
||||
"xpack.kubernetesSecurity.treeNav.node": "{isPlural, select, other {节点}}",
|
||||
"xpack.kubernetesSecurity.treeNav.pod": "{isPlural, select, other {Pod}}",
|
||||
"xpack.kubernetesSecurity.treeNavigation.collapse": "折叠树形导航",
|
||||
"xpack.kubernetesSecurity.treeNavigation.empty": "没有可用数据",
|
||||
"xpack.kubernetesSecurity.treeNavigation.expand": "展开树形导航",
|
||||
"xpack.kubernetesSecurity.treeNavigation.loading": "正在加载",
|
||||
"xpack.kubernetesSecurity.treeNavigation.loadMore": "显示更多 {name}",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.clusterId": "集群",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.clusterName": "集群",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.containerImage": "容器映像",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.namespace": "命名空间",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.node": "节点",
|
||||
"xpack.kubernetesSecurity.treeView.breadcrumb.pod": "Pod",
|
||||
"xpack.kubernetesSecurity.treeView.empty.description": "尝试搜索更长的时间段或修改您的搜索",
|
||||
"xpack.kubernetesSecurity.treeView.empty.title": "没有任何结果匹配您的搜索条件",
|
||||
"xpack.kubernetesSecurity.treeView.infrastructureView": "基础架构视图",
|
||||
"xpack.kubernetesSecurity.treeView.logicalView": "逻辑视图",
|
||||
"xpack.kubernetesSecurity.treeView.switherLegend": "您可以在逻辑视图与基础架构视图之间切换",
|
||||
"xpack.lens.action.exploreInDiscover": "在 Discover 中浏览",
|
||||
"xpack.lens.AggBasedLabel": "基于聚合的可视化",
|
||||
"xpack.lens.app.addToLibrary": "保存到库",
|
||||
|
@ -34074,7 +34031,6 @@
|
|||
"xpack.securitySolution.appLinks.hosts.risk": "主机风险",
|
||||
"xpack.securitySolution.appLinks.hosts.sessions": "会话",
|
||||
"xpack.securitySolution.appLinks.hosts.uncommonProcesses": "不常见进程",
|
||||
"xpack.securitySolution.appLinks.kubernetesDescription": "提供 Kubernetes 工作负载和会话数据的交互式可视化。",
|
||||
"xpack.securitySolution.appLinks.manage": "管理",
|
||||
"xpack.securitySolution.appLinks.ml.keyword": "Machine Learning",
|
||||
"xpack.securitySolution.appLinks.ml.title": "Machine Learning",
|
||||
|
@ -38233,13 +38189,6 @@
|
|||
"xpack.securitySolution.kpiNetwork.uniquePrivateIps.title": "唯一专用 IP",
|
||||
"xpack.securitySolution.kpiUsers.totalUsers.errorSearchDescription": "搜索总体用户 KPI 时发生错误",
|
||||
"xpack.securitySolution.kpiUsers.totalUsers.title": "用户",
|
||||
"xpack.securitySolution.kubernetes.columnContainer": "容器",
|
||||
"xpack.securitySolution.kubernetes.columnEntryUser": "用户 ID",
|
||||
"xpack.securitySolution.kubernetes.columnExecutable": "可执行",
|
||||
"xpack.securitySolution.kubernetes.columnInteractive": "交互",
|
||||
"xpack.securitySolution.kubernetes.columnNode": "节点",
|
||||
"xpack.securitySolution.kubernetes.columnPod": "Pod",
|
||||
"xpack.securitySolution.kubernetes.columnSessionStart": "开始日期",
|
||||
"xpack.securitySolution.landing.threatHunting.hostsDescription": "所有主机和主机相关安全事件的全面概览。",
|
||||
"xpack.securitySolution.lastEventTime.failSearchDescription": "无法对上次事件时间执行搜索",
|
||||
"xpack.securitySolution.lensEmbeddable.NoDataToDisplay.title": "没有可显示的数据",
|
||||
|
|
|
@ -23,8 +23,7 @@
|
|||
"kibanaReact",
|
||||
"cloud",
|
||||
"security",
|
||||
"licensing",
|
||||
"kubernetesSecurity"
|
||||
"licensing"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"usageCollection"
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { KUBERNETES_PATH, KUBERNETES_TITLE } from '@kbn/kubernetes-security-plugin/public';
|
||||
import type { CloudDefendPage, CloudDefendPageNavigationItem } from './types';
|
||||
|
||||
const NAV_ITEMS_NAMES = {
|
||||
|
@ -24,9 +23,4 @@ export const cloudDefendPages: Record<CloudDefendPage, CloudDefendPageNavigation
|
|||
path: `${CLOUD_DEFEND_BASE_PATH}/policies`,
|
||||
id: 'cloud_defend-policies',
|
||||
},
|
||||
dashboard: {
|
||||
name: KUBERNETES_TITLE,
|
||||
path: KUBERNETES_PATH,
|
||||
id: 'kubernetes_security-dashboard',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -14,10 +14,10 @@ export interface CloudDefendPageNavigationItem extends CloudDefendNavigationItem
|
|||
id: CloudDefendPageId;
|
||||
}
|
||||
|
||||
export type CloudDefendPage = 'policies' | 'dashboard';
|
||||
export type CloudDefendPage = 'policies';
|
||||
|
||||
/**
|
||||
* All the IDs for the cloud defend pages.
|
||||
* This needs to match the cloud defend page entries in `SecurityPageName` in `x-pack/solutions/security/plugins/security_solution/common/constants.ts`.
|
||||
*/
|
||||
export type CloudDefendPageId = 'cloud_defend-policies' | 'kubernetes_security-dashboard';
|
||||
export type CloudDefendPageId = 'cloud_defend-policies';
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { type CustomAssetsAccordionProps, CustomAssetsAccordion } from '@kbn/fleet-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../../common/hooks/use_kibana';
|
||||
import { cloudDefendPages } from '../../common/navigation/constants';
|
||||
|
||||
const SECURITY_APP_NAME = 'securitySolutionUI';
|
||||
|
||||
export const CloudDefendCustomAssetsExtension = () => {
|
||||
const { application } = useKibana().services;
|
||||
|
||||
const views: CustomAssetsAccordionProps['views'] = [
|
||||
{
|
||||
name: cloudDefendPages.dashboard.name,
|
||||
url: application.getUrlForApp(SECURITY_APP_NAME, { path: cloudDefendPages.dashboard.path }),
|
||||
description: i18n.translate(
|
||||
'xpack.cloudDefend.createPackagePolicy.customAssetsTab.dashboardViewLabel',
|
||||
{ defaultMessage: 'View k8s dashboard' }
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return <CustomAssetsAccordion views={views} initialIsOpen />;
|
||||
};
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { CloudDefendCustomAssetsExtension as default };
|
|
@ -23,10 +23,6 @@ const LazyPolicyReplaceDefineStepExtension = lazy(
|
|||
() => import('./components/fleet_extensions/package_policy_replace_define_step_extension')
|
||||
);
|
||||
|
||||
const LazyCustomAssets = lazy(
|
||||
() => import('./components/fleet_extensions/custom_assets_extension')
|
||||
);
|
||||
|
||||
const RouterLazy = lazy(() => import('./application/router'));
|
||||
const Router = (props: CloudDefendRouterProps) => (
|
||||
<Suspense fallback={<LoadingState />}>
|
||||
|
@ -62,12 +58,6 @@ export class CloudDefendPlugin
|
|||
Component: LazyPolicyReplaceDefineStepExtension,
|
||||
});
|
||||
|
||||
plugins.fleet.registerExtension({
|
||||
package: INTEGRATION_PACKAGE_NAME,
|
||||
view: 'package-detail-assets',
|
||||
Component: LazyCustomAssets,
|
||||
});
|
||||
|
||||
const CloudDefendRouter = (props: CloudDefendRouterProps) => (
|
||||
<KibanaContextProvider services={{ ...core, ...plugins }}>
|
||||
<RedirectAppLinks coreStart={core}>
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
"@kbn/es-types",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/kubernetes-security-plugin",
|
||||
"@kbn/core-http-router-server-mocks",
|
||||
"@kbn/core-elasticsearch-server",
|
||||
"@kbn/code-editor",
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-definitions": 0
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
# Kubernetes Security
|
||||
This plugin provides interactive visualizations of your Kubernetes workload and session data.
|
||||
|
||||
## Overview
|
||||
Allow users to explore the data stream from k8s environment that being monitored by Elastic Agent(+ endpoint integration) in a session view with cloud and k8s context. For more context, see internal [doc](https://github.com/elastic/security-team/issues/3337).
|
||||
|
||||
This plugin is currently being used as a part of Security Solution features under the `/app/security/kubernetes` page.
|
||||
|
||||
## API
|
||||
|
||||
#### `getKubernetesPage`
|
||||
Returns the kubernetes page.
|
||||
Parameters
|
||||
| Property | Description | Type |
|
||||
| ----------------------- | ----------------- | ------ |
|
||||
| kubernetesSecurityDeps | Parameters object | object |
|
||||
|
||||
`kubernetesSecurityDeps`
|
||||
| Property | Description | Type |
|
||||
| ------------------- | ------------------------------------------------------------- | --------- |
|
||||
| filter | The global filter component used across pages in Kibana | ReactNode |
|
||||
| renderSessionsView | Function to render sessions view table | function |
|
||||
| indexPattern | Index pattern used for the data source in the Kubernetes page | object |
|
||||
| globalFilter | Includes query and timerange used for filtering | object |
|
||||
|
||||
`indexPattern`
|
||||
| Property | Description | Type |
|
||||
| --------- | ----------------------------------- | -------------------------------------------- |
|
||||
| fields | A list of `FieldSpec` | `FieldSpec[]` from `@kbn/data-plugin/common` |
|
||||
| title | Index pattern string representation | string |
|
||||
|
||||
`globalFilter`
|
||||
| Property | Description | Type |
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
||||
| filterQuery | Stringified Elasticsearch filter query. See [doc](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html). | Optional, string |
|
||||
| startDate | Start date time of timerange filter, in ISO format | string |
|
||||
| endDate | End date time of timerange filter, in ISO format | string |
|
||||
|
||||
|
||||
## Page preview
|
||||

|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const KUBERNETES_PATH = '/kubernetes' as const;
|
||||
export const KUBERNETES_TITLE = 'Kubernetes';
|
||||
export const LOCAL_STORAGE_HIDE_WIDGETS_KEY = 'kubernetesSecurity:shouldHideWidgets';
|
||||
export const LOCAL_STORAGE_TREE_NAV_KEY = 'kubernetesSecurity:treeNavSelection';
|
||||
|
||||
export const CURRENT_API_VERSION = '1';
|
||||
export const AGGREGATE_ROUTE = '/internal/kubernetes_security/aggregate';
|
||||
export const COUNT_ROUTE = '/internal/kubernetes_security/count';
|
||||
export const MULTI_TERMS_AGGREGATE_ROUTE = '/internal/kubernetes_security/multi_terms_aggregate';
|
||||
|
||||
export const AGGREGATE_PAGE_SIZE = 10;
|
||||
|
||||
// so, bucket sort can only page through what we request at the top level agg, which means there is a ceiling to how many aggs we can page through.
|
||||
// we should also test this approach at scale.
|
||||
export const AGGREGATE_MAX_BUCKETS = 2000;
|
||||
|
||||
// react-query caching keys
|
||||
export const QUERY_KEY_PERCENT_WIDGET = 'kubernetesSecurityPercentWidget';
|
||||
export const QUERY_KEY_COUNT_WIDGET = 'kubernetesSecurityCountWidget';
|
||||
export const QUERY_KEY_CONTAINER_NAME_WIDGET = 'kubernetesSecurityContainerNameWidget';
|
||||
export const QUERY_KEY_PROCESS_EVENTS = 'kubernetesSecurityProcessEvents';
|
||||
export const QUERY_KEY_AGENT_ID = 'kubernetesSecurityAgentId';
|
||||
|
||||
// ECS fields
|
||||
export const ENTRY_LEADER_INTERACTIVE = 'process.entry_leader.interactive';
|
||||
export const ENTRY_LEADER_USER_ID = 'process.entry_leader.user.id';
|
||||
export const ENTRY_LEADER_ENTITY_ID = 'process.entry_leader.entity_id';
|
||||
|
||||
export const ORCHESTRATOR_CLUSTER_ID = 'orchestrator.cluster.id';
|
||||
export const ORCHESTRATOR_CLUSTER_NAME = 'orchestrator.cluster.name';
|
||||
export const ORCHESTRATOR_NAMESPACE = 'orchestrator.namespace';
|
||||
export const CLOUD_INSTANCE_NAME = 'cloud.instance.name';
|
||||
export const ORCHESTRATOR_RESOURCE_ID = 'orchestrator.resource.name';
|
||||
export const CONTAINER_IMAGE_NAME = 'container.image.name';
|
||||
|
||||
export const COUNT_WIDGET_KEY_CLUSTERS = 'CountClustersWidget';
|
||||
export const COUNT_WIDGET_KEY_NAMESPACE = 'CountNamespaceWidgets';
|
||||
export const COUNT_WIDGET_KEY_NODES = 'CountNodesWidgets';
|
||||
export const COUNT_WIDGET_KEY_PODS = 'CountPodsWidgets';
|
||||
export const COUNT_WIDGET_KEY_CONTAINER_IMAGES = 'CountContainerImagesWidgets';
|
||||
|
||||
export const DEFAULT_FILTER = {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
exists: {
|
||||
field: ORCHESTRATOR_CLUSTER_ID,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_FILTER_QUERY = JSON.stringify({
|
||||
bool: {
|
||||
must: [],
|
||||
filter: [DEFAULT_FILTER],
|
||||
should: [],
|
||||
must_not: [],
|
||||
},
|
||||
});
|
|
@ -1,134 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SEARCH_GROUP_CLUSTER = i18n.translate('xpack.kubernetesSecurity.searchGroup.cluster', {
|
||||
defaultMessage: 'Cluster',
|
||||
});
|
||||
|
||||
export const SEARCH_GROUP_GROUP_BY = i18n.translate(
|
||||
'xpack.kubernetesSecurity.searchGroup.groupBy',
|
||||
{
|
||||
defaultMessage: 'Group by',
|
||||
}
|
||||
);
|
||||
|
||||
export const SEARCH_GROUP_SORT_BY = i18n.translate('xpack.kubernetesSecurity.searchGroup.sortBy', {
|
||||
defaultMessage: 'Sort by',
|
||||
});
|
||||
|
||||
export const TREE_VIEW_LOGICAL_VIEW = i18n.translate(
|
||||
'xpack.kubernetesSecurity.treeView.logicalView',
|
||||
{
|
||||
defaultMessage: 'Logical view',
|
||||
}
|
||||
);
|
||||
|
||||
export const TREE_VIEW_INFRASTRUCTURE_VIEW = i18n.translate(
|
||||
'xpack.kubernetesSecurity.treeView.infrastructureView',
|
||||
{
|
||||
defaultMessage: 'Infrastructure view',
|
||||
}
|
||||
);
|
||||
|
||||
export const TREE_VIEW_SWITCHER_LEGEND = i18n.translate(
|
||||
'xpack.kubernetesSecurity.treeView.switherLegend',
|
||||
{
|
||||
defaultMessage: 'You can switch between the Logical and Infrastructure view',
|
||||
}
|
||||
);
|
||||
|
||||
export const TREE_NAVIGATION_LOADING = i18n.translate(
|
||||
'xpack.kubernetesSecurity.treeNavigation.loading',
|
||||
{
|
||||
defaultMessage: 'Loading',
|
||||
}
|
||||
);
|
||||
export const TREE_NAVIGATION_EMPTY = i18n.translate(
|
||||
'xpack.kubernetesSecurity.treeNavigation.empty',
|
||||
{
|
||||
defaultMessage: 'No data available',
|
||||
}
|
||||
);
|
||||
export const TREE_NAVIGATION_SHOW_MORE = (name: string) =>
|
||||
i18n.translate('xpack.kubernetesSecurity.treeNavigation.loadMore', {
|
||||
values: { name },
|
||||
defaultMessage: 'Show more {name}',
|
||||
});
|
||||
|
||||
export const TREE_NAVIGATION_COLLAPSE = i18n.translate(
|
||||
'xpack.kubernetesSecurity.treeNavigation.collapse',
|
||||
{
|
||||
defaultMessage: 'Collapse tree navigation',
|
||||
}
|
||||
);
|
||||
|
||||
export const TREE_NAVIGATION_EXPAND = i18n.translate(
|
||||
'xpack.kubernetesSecurity.treeNavigation.expand',
|
||||
{
|
||||
defaultMessage: 'Expand tree navigation',
|
||||
}
|
||||
);
|
||||
|
||||
export const CHART_TOGGLE_SHOW = i18n.translate('xpack.kubernetesSecurity.chartsToggle.show', {
|
||||
defaultMessage: 'Show charts',
|
||||
});
|
||||
|
||||
export const CHART_TOGGLE_HIDE = i18n.translate('xpack.kubernetesSecurity.chartsToggle.hide', {
|
||||
defaultMessage: 'Hide charts',
|
||||
});
|
||||
|
||||
export const COUNT_WIDGET_CLUSTERS = i18n.translate(
|
||||
'xpack.kubernetesSecurity.countWidget.clusters',
|
||||
{
|
||||
defaultMessage: 'Clusters',
|
||||
}
|
||||
);
|
||||
|
||||
export const COUNT_WIDGET_NAMESPACE = i18n.translate(
|
||||
'xpack.kubernetesSecurity.countWidget.namespace',
|
||||
{
|
||||
defaultMessage: 'Namespace',
|
||||
}
|
||||
);
|
||||
|
||||
export const COUNT_WIDGET_NODES = i18n.translate('xpack.kubernetesSecurity.countWidget.nodes', {
|
||||
defaultMessage: 'Nodes',
|
||||
});
|
||||
|
||||
export const COUNT_WIDGET_PODS = i18n.translate('xpack.kubernetesSecurity.countWidget.pods', {
|
||||
defaultMessage: 'Pods',
|
||||
});
|
||||
|
||||
export const COUNT_WIDGET_CONTAINER_IMAGES = i18n.translate(
|
||||
'xpack.kubernetesSecurity.countWidget.containerImages',
|
||||
{
|
||||
defaultMessage: 'Container images',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONTAINER_NAME_SESSION = i18n.translate(
|
||||
'xpack.kubernetesSecurity.containerNameWidget.containerImage',
|
||||
{
|
||||
defaultMessage: 'Container image',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONTAINER_NAME_SESSION_COUNT_COLUMN = i18n.translate(
|
||||
'xpack.kubernetesSecurity.containerNameWidget.containerImageCountColumn',
|
||||
{
|
||||
defaultMessage: 'Session count',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONTAINER_NAME_SESSION_ARIA_LABEL = i18n.translate(
|
||||
'xpack.kubernetesSecurity.containerNameWidget.containerImageAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Container name session widget',
|
||||
}
|
||||
);
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export type {
|
||||
AggregateResult,
|
||||
AggregateBucketPaginationResult,
|
||||
MultiTermsAggregateGroupBy,
|
||||
MultiTermsAggregateResult,
|
||||
MultiTermsAggregateBucketPaginationResult,
|
||||
MultiTermsBucket,
|
||||
} from './latest';
|
||||
|
||||
import * as v1 from './v1';
|
||||
export type { v1 };
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
export * from './v1';
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
interface Aggregate {
|
||||
key: string | number;
|
||||
doc_count: number;
|
||||
}
|
||||
|
||||
interface Bucket extends Aggregate {
|
||||
key_as_string?: string;
|
||||
count_by_aggs?: {
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AggregateResult {
|
||||
buckets: Bucket[];
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
|
||||
export interface AggregateBucketPaginationResult {
|
||||
buckets: Bucket[];
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
|
||||
export interface MultiTermsAggregateGroupBy {
|
||||
field: string;
|
||||
maybe?: string;
|
||||
}
|
||||
|
||||
interface MultiTermsAggregate {
|
||||
key: Array<string | number | boolean>;
|
||||
doc_count: number;
|
||||
}
|
||||
|
||||
export interface MultiTermsBucket extends MultiTermsAggregate {
|
||||
key_as_string?: string;
|
||||
count_by_aggs?: {
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MultiTermsAggregateResult {
|
||||
buckets: Bucket[];
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
|
||||
export interface MultiTermsAggregateBucketPaginationResult {
|
||||
buckets: Bucket[];
|
||||
hasNextPage: boolean;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/x-pack/solutions/security/plugins/kubernetes_security'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/solutions/security/plugins/kubernetes_security',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/solutions/security/plugins/kubernetes_security/{common,public,server}/**/*.{ts,tsx}',
|
||||
],
|
||||
setupFiles: ['jest-canvas-mock'],
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/kubernetes-security-plugin",
|
||||
"owner": [
|
||||
"@elastic/kibana-cloud-security-posture"
|
||||
],
|
||||
"group": "security",
|
||||
"visibility": "private",
|
||||
"plugin": {
|
||||
"id": "kubernetesSecurity",
|
||||
"browser": true,
|
||||
"server": true,
|
||||
"requiredPlugins": [
|
||||
"data",
|
||||
"timelines",
|
||||
"ruleRegistry",
|
||||
"sessionView"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"kibanaReact"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"author": "Elastic",
|
||||
"name": "@kbn/kubernetes-security-plugin",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "Elastic License 2.0",
|
||||
"scripts": {
|
||||
"test:jest": "node ../../../../scripts/jest",
|
||||
"test:coverage": "node ../../../../scripts/jest --coverage"
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { CHART_TOGGLE_SHOW, CHART_TOGGLE_HIDE } from '../../../common/translations';
|
||||
import { ChartsToggle, TOGGLE_TEST_ID } from '.';
|
||||
|
||||
describe('ChartsToggle component', () => {
|
||||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
const handleToggleHideCharts = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
});
|
||||
|
||||
describe('When ChartsToggle is mounted', () => {
|
||||
it('show "hide charts" text when shouldHideCharts is false', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<ChartsToggle shouldHideCharts={false} handleToggleHideCharts={handleToggleHideCharts} />
|
||||
);
|
||||
|
||||
expect(renderResult.getByText(CHART_TOGGLE_HIDE)).toBeVisible();
|
||||
});
|
||||
it('show "show charts" text when shouldHideCharts is true', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<ChartsToggle shouldHideCharts={true} handleToggleHideCharts={handleToggleHideCharts} />
|
||||
);
|
||||
|
||||
expect(renderResult.getByText(CHART_TOGGLE_SHOW)).toBeVisible();
|
||||
});
|
||||
it('shouldHideCharts defaults to false when not provided', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<ChartsToggle handleToggleHideCharts={handleToggleHideCharts} />
|
||||
);
|
||||
|
||||
expect(renderResult.getByText(CHART_TOGGLE_HIDE)).toBeVisible();
|
||||
});
|
||||
it('clicking the toggle fires the callback', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<ChartsToggle handleToggleHideCharts={handleToggleHideCharts} />
|
||||
);
|
||||
|
||||
renderResult.queryByTestId(TOGGLE_TEST_ID)?.click();
|
||||
expect(handleToggleHideCharts).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { CHART_TOGGLE_SHOW, CHART_TOGGLE_HIDE } from '../../../common/translations';
|
||||
|
||||
export const TOGGLE_TEST_ID = 'kubernetesSecurity:chartToggle';
|
||||
|
||||
interface ChartsToggleDeps {
|
||||
handleToggleHideCharts: () => void;
|
||||
shouldHideCharts?: boolean;
|
||||
}
|
||||
|
||||
export const ChartsToggle = ({
|
||||
handleToggleHideCharts,
|
||||
shouldHideCharts = false,
|
||||
}: ChartsToggleDeps) => (
|
||||
<EuiButtonEmpty
|
||||
onClick={handleToggleHideCharts}
|
||||
iconType={shouldHideCharts ? 'eye' : 'eyeClosed'}
|
||||
data-test-subj={TOGGLE_TEST_ID}
|
||||
>
|
||||
{shouldHideCharts ? CHART_TOGGLE_SHOW : CHART_TOGGLE_HIDE}
|
||||
</EuiButtonEmpty>
|
||||
);
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { ContainerNameRow } from './container_name_row';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
|
||||
const TEST_NAME = 'TEST ROW';
|
||||
const TEST_BUTTON_FILTER = <div>Filter In</div>;
|
||||
const TEST_BUTTON_FILTER_OUT = <div>Filter Out</div>;
|
||||
const TEST_BUTTON_COPY = <div>Copy</div>;
|
||||
|
||||
describe('ContainerNameRow component with valid row', () => {
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
const mockedContext = createAppRootMockRenderer();
|
||||
const render: () => ReturnType<AppContextTestRender['render']> = () =>
|
||||
(renderResult = mockedContext.render(
|
||||
<ContainerNameRow
|
||||
name={TEST_NAME}
|
||||
filterButtonIn={TEST_BUTTON_FILTER}
|
||||
filterButtonOut={TEST_BUTTON_FILTER_OUT}
|
||||
copyToClipboardButton={TEST_BUTTON_COPY}
|
||||
/>
|
||||
));
|
||||
|
||||
it('should show the row element as well as the pop up filter button when mouse hovers above it', async () => {
|
||||
render();
|
||||
expect(renderResult.getByText(TEST_NAME)).toBeVisible();
|
||||
fireEvent.mouseOver(renderResult.queryByText(TEST_NAME)!);
|
||||
expect(renderResult.getByText('Filter In')).toBeVisible();
|
||||
expect(renderResult.getByText('Filter Out')).toBeVisible();
|
||||
expect(renderResult.getByText('Copy')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should show the row element but not the pop up filter button outside mouse hover', async () => {
|
||||
render();
|
||||
expect(renderResult.getByText(TEST_NAME)).toBeVisible();
|
||||
expect(renderResult.queryByText('Filter In')).toBeFalsy();
|
||||
expect(renderResult.queryByText('Filter Out')).toBeFalsy();
|
||||
expect(renderResult.queryByText('Copy')).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
export interface ContainerNameRowDeps {
|
||||
name: string;
|
||||
filterButtonIn?: ReactNode;
|
||||
filterButtonOut?: ReactNode;
|
||||
copyToClipboardButton?: ReactNode;
|
||||
}
|
||||
|
||||
export const ROW_TEST_ID = 'kubernetesSecurity:containerNameSessionRow';
|
||||
|
||||
export const ContainerNameRow = ({
|
||||
name,
|
||||
filterButtonIn,
|
||||
filterButtonOut,
|
||||
copyToClipboardButton,
|
||||
}: ContainerNameRowDeps) => {
|
||||
const [isHover, setIsHover] = useState<boolean>(false);
|
||||
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<EuiFlexItem
|
||||
onMouseEnter={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
data-test-subj={ROW_TEST_ID}
|
||||
css={styles.flexWidth}
|
||||
>
|
||||
<EuiText size="xs" css={styles.dataInfo}>
|
||||
<div css={styles.truncate}>{name}</div>
|
||||
{isHover && (
|
||||
<div css={styles.filters}>
|
||||
{filterButtonIn}
|
||||
{filterButtonOut}
|
||||
{copyToClipboardButton}
|
||||
</div>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
QUERY_KEY_CONTAINER_NAME_WIDGET,
|
||||
AGGREGATE_ROUTE,
|
||||
CURRENT_API_VERSION,
|
||||
} from '../../../common/constants';
|
||||
import { AggregateResult } from '../../../common/types';
|
||||
|
||||
export const useFetchContainerNameData = (
|
||||
filterQuery: string,
|
||||
widgetKey: string,
|
||||
groupBy: string,
|
||||
countBy?: string,
|
||||
index?: string,
|
||||
sortByCount?: string,
|
||||
pageNumber?: number
|
||||
) => {
|
||||
const { http } = useKibana<CoreStart>().services;
|
||||
const cachingKeys = [
|
||||
QUERY_KEY_CONTAINER_NAME_WIDGET,
|
||||
widgetKey,
|
||||
filterQuery,
|
||||
groupBy,
|
||||
countBy,
|
||||
sortByCount,
|
||||
pageNumber,
|
||||
];
|
||||
const query = useInfiniteQuery(
|
||||
cachingKeys,
|
||||
async ({ pageParam = 0 }) => {
|
||||
const res = await http.get<AggregateResult>(AGGREGATE_ROUTE, {
|
||||
version: CURRENT_API_VERSION,
|
||||
query: {
|
||||
query: filterQuery,
|
||||
groupBy,
|
||||
countBy,
|
||||
page: pageParam,
|
||||
index,
|
||||
sortByCount,
|
||||
},
|
||||
});
|
||||
return res;
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
getNextPageParam: (lastPage, pages) => (lastPage.hasNextPage ? pages.length : undefined),
|
||||
}
|
||||
);
|
||||
return query;
|
||||
};
|
|
@ -1,138 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ENTRY_LEADER_ENTITY_ID, CONTAINER_IMAGE_NAME } from '../../../common/constants';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { GlobalFilter } from '../../types';
|
||||
import {
|
||||
ContainerNameWidget,
|
||||
LOADING_TEST_ID,
|
||||
NAME_COLUMN_TEST_ID,
|
||||
COUNT_COLUMN_TEST_ID,
|
||||
CONTAINER_NAME_TABLE_TEST_ID,
|
||||
} from '.';
|
||||
import { useFetchContainerNameData } from './hooks';
|
||||
import { ROW_TEST_ID } from './container_name_row';
|
||||
|
||||
const TABLE_SORT_BUTTON_ID = 'tableHeaderSortButton';
|
||||
|
||||
const TITLE = 'Container image';
|
||||
const GLOBAL_FILTER: GlobalFilter = {
|
||||
endDate: '2022-06-15T14:15:25.777Z',
|
||||
filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}',
|
||||
startDate: '2022-05-15T14:15:25.777Z',
|
||||
};
|
||||
const MOCK_DATA = {
|
||||
pages: [
|
||||
{
|
||||
buckets: [
|
||||
{ key: 'Container A', doc_count: 295, count_by_aggs: { value: 1 } },
|
||||
{ key: 'Container B', doc_count: 295, count_by_aggs: { value: 3 } },
|
||||
{ key: 'Container C', doc_count: 295, count_by_aggs: { value: 2 } },
|
||||
{ key: 'Container D', doc_count: 295, count_by_aggs: { value: 4 } },
|
||||
{ key: 'Container E', doc_count: 295, count_by_aggs: { value: 1 } },
|
||||
{ key: 'Container F', doc_count: 295, count_by_aggs: { value: 1 } },
|
||||
{ key: 'Container G', doc_count: 295, count_by_aggs: { value: 0 } },
|
||||
{ key: 'Container H', doc_count: 295, count_by_aggs: { value: 1 } },
|
||||
{ key: 'Container J', doc_count: 295, count_by_aggs: { value: 1 } },
|
||||
{ key: 'Container K', doc_count: 295, count_by_aggs: { value: 1 } },
|
||||
{ key: 'Container L', doc_count: 295, count_by_aggs: { value: 5 } },
|
||||
],
|
||||
hasNextPage: true,
|
||||
},
|
||||
{
|
||||
buckets: [
|
||||
{ key: 'Container A2', doc_count: 295, count_by_aggs: { value: 2 } },
|
||||
{ key: 'Container B2', doc_count: 295, count_by_aggs: { value: 1 } },
|
||||
{ key: 'Container C2', doc_count: 295, count_by_aggs: { value: 6 } },
|
||||
{ key: 'Container D2', doc_count: 295, count_by_aggs: { value: 1 } },
|
||||
{ key: 'Container E2', doc_count: 295, count_by_aggs: { value: 3 } },
|
||||
{ key: 'Container F2', doc_count: 295, count_by_aggs: { value: 1 } },
|
||||
],
|
||||
hasNextPage: false,
|
||||
},
|
||||
],
|
||||
pageParams: [undefined],
|
||||
};
|
||||
const MOCK_DATA_VIEW_ID = 'dataViewId';
|
||||
|
||||
jest.mock('../../hooks/use_filter', () => ({
|
||||
useSetFilter: () => ({
|
||||
getFilterForValueButton: jest.fn(),
|
||||
getFilterOutValueButton: jest.fn(),
|
||||
getCopyButton: jest.fn(),
|
||||
filterManager: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('./hooks');
|
||||
const mockUseFetchData = useFetchContainerNameData as jest.Mock;
|
||||
|
||||
describe('ContainerNameWidget component', () => {
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
const mockedContext = createAppRootMockRenderer();
|
||||
const render: () => ReturnType<AppContextTestRender['render']> = () =>
|
||||
(renderResult = mockedContext.render(
|
||||
<ContainerNameWidget
|
||||
widgetKey="containerNameSessions"
|
||||
globalFilter={GLOBAL_FILTER}
|
||||
groupedBy={CONTAINER_IMAGE_NAME}
|
||||
countBy={ENTRY_LEADER_ENTITY_ID}
|
||||
dataViewId={MOCK_DATA_VIEW_ID}
|
||||
/>
|
||||
));
|
||||
|
||||
describe('When ContainerNameWidget is mounted', () => {
|
||||
describe('with data', () => {
|
||||
beforeEach(() => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: MOCK_DATA,
|
||||
isFetchingNextPage: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should show the table, table title, table columns, sort button', async () => {
|
||||
render();
|
||||
expect(renderResult.queryByTestId(CONTAINER_NAME_TABLE_TEST_ID)).toBeVisible();
|
||||
expect(renderResult.queryAllByTestId(TABLE_SORT_BUTTON_ID)).toHaveLength(1);
|
||||
expect(renderResult.queryAllByTestId(NAME_COLUMN_TEST_ID)).toHaveLength(17);
|
||||
expect(renderResult.queryAllByTestId(COUNT_COLUMN_TEST_ID)).toHaveLength(17);
|
||||
});
|
||||
|
||||
it('should show data value names and value', async () => {
|
||||
render();
|
||||
expect(renderResult.queryAllByTestId(ROW_TEST_ID)).toHaveLength(17);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without data ', () => {
|
||||
it('should show no items found text', async () => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: undefined,
|
||||
isFetchingNextPage: false,
|
||||
}));
|
||||
render();
|
||||
expect(renderResult.getByText(TITLE)).toBeVisible();
|
||||
expect(renderResult.getByText('No items found')).toBeVisible();
|
||||
expect(renderResult.getByTestId(CONTAINER_NAME_TABLE_TEST_ID)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loading data', () => {
|
||||
it('should show progress bar', async () => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: MOCK_DATA,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: true,
|
||||
}));
|
||||
render();
|
||||
expect(renderResult.getByTestId(LOADING_TEST_ID)).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,270 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useMemo, useState, useRef, useCallback } from 'react';
|
||||
import { EuiBasicTable, EuiTableSortingType, EuiProgress, EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { useStyles } from './styles';
|
||||
import { ContainerNameRow } from './container_name_row';
|
||||
import type { IndexPattern, GlobalFilter } from '../../types';
|
||||
import { useSetFilter, useScroll } from '../../hooks';
|
||||
import { addTimerangeAndDefaultFilterToQuery } from '../../utils/add_timerange_and_default_filter_to_query';
|
||||
import { useFetchContainerNameData } from './hooks';
|
||||
import { CONTAINER_IMAGE_NAME } from '../../../common/constants';
|
||||
import {
|
||||
CONTAINER_NAME_SESSION,
|
||||
CONTAINER_NAME_SESSION_COUNT_COLUMN,
|
||||
CONTAINER_NAME_SESSION_ARIA_LABEL,
|
||||
} from '../../../common/translations';
|
||||
import { addCommasToNumber } from '../../utils/add_commas_to_number';
|
||||
|
||||
export const LOADING_TEST_ID = 'kubernetesSecurity:containerNameWidgetLoading';
|
||||
export const NAME_COLUMN_TEST_ID = 'kubernetesSecurity:containerImageNameSessionNameColumn';
|
||||
export const COUNT_COLUMN_TEST_ID = 'kubernetesSecurity:containerImageNameSessionCountColumn';
|
||||
export const CONTAINER_NAME_TABLE_TEST_ID = 'kubernetesSecurity:containerNameSessionTable';
|
||||
|
||||
export interface ContainerNameWidgetDataValueMap {
|
||||
key: string;
|
||||
doc_count: number;
|
||||
count_by_aggs: {
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ContainerNameArrayDataValue {
|
||||
name: string;
|
||||
count: string;
|
||||
}
|
||||
|
||||
export interface ContainerNameWidgetDeps {
|
||||
widgetKey: string;
|
||||
indexPattern?: IndexPattern;
|
||||
globalFilter: GlobalFilter;
|
||||
groupedBy: string;
|
||||
countBy?: string;
|
||||
dataViewId?: string;
|
||||
}
|
||||
|
||||
interface FilterButtons {
|
||||
filterForButtons: ReactNode[];
|
||||
filterOutButtons: ReactNode[];
|
||||
}
|
||||
|
||||
interface CopyButtons {
|
||||
copyButtons: ReactNode[];
|
||||
}
|
||||
|
||||
export const ContainerNameWidget = ({
|
||||
dataViewId,
|
||||
widgetKey,
|
||||
indexPattern,
|
||||
globalFilter,
|
||||
groupedBy,
|
||||
countBy,
|
||||
}: ContainerNameWidgetDeps) => {
|
||||
const [sortField, setSortField] = useState('count');
|
||||
const [sortDirection, setSortDirection] = useState('desc');
|
||||
const styles = useStyles();
|
||||
|
||||
const filterQueryWithTimeRange = useMemo(() => {
|
||||
return addTimerangeAndDefaultFilterToQuery(
|
||||
globalFilter.filterQuery,
|
||||
globalFilter.startDate,
|
||||
globalFilter.endDate
|
||||
);
|
||||
}, [globalFilter.filterQuery, globalFilter.startDate, globalFilter.endDate]);
|
||||
|
||||
const { data, fetchNextPage, isFetchingNextPage, isLoading } = useFetchContainerNameData(
|
||||
filterQueryWithTimeRange,
|
||||
widgetKey,
|
||||
groupedBy,
|
||||
countBy,
|
||||
indexPattern?.title,
|
||||
sortDirection
|
||||
);
|
||||
|
||||
const onTableChange = useCallback(({ sort = {} }: any) => {
|
||||
// @ts-ignore
|
||||
const { field: sortingField, direction: sortingDirection } = sort;
|
||||
|
||||
setSortField(sortingField);
|
||||
setSortDirection(sortingDirection);
|
||||
}, []);
|
||||
|
||||
const sorting: EuiTableSortingType<ContainerNameArrayDataValue> = {
|
||||
sort: {
|
||||
field: sortField as keyof ContainerNameArrayDataValue,
|
||||
direction: sortDirection as 'desc' | 'asc',
|
||||
},
|
||||
enableAllColumns: true,
|
||||
};
|
||||
|
||||
const { getFilterForValueButton, getFilterOutValueButton, getCopyButton, filterManager } =
|
||||
useSetFilter();
|
||||
const filterButtons = useMemo((): FilterButtons => {
|
||||
const result: FilterButtons = {
|
||||
filterForButtons:
|
||||
data?.pages
|
||||
?.map((aggsData) => {
|
||||
return aggsData?.buckets.map((aggData) => {
|
||||
return getFilterForValueButton({
|
||||
field: CONTAINER_IMAGE_NAME,
|
||||
filterManager,
|
||||
size: 'xs',
|
||||
onClick: () => {},
|
||||
onFilterAdded: () => {},
|
||||
ownFocus: false,
|
||||
showTooltip: true,
|
||||
value: aggData.key as string,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
})
|
||||
.flat() || [],
|
||||
|
||||
filterOutButtons:
|
||||
data?.pages
|
||||
?.map((aggsData) => {
|
||||
return aggsData?.buckets.map((aggData) => {
|
||||
return getFilterOutValueButton({
|
||||
field: CONTAINER_IMAGE_NAME,
|
||||
filterManager,
|
||||
size: 'xs',
|
||||
onClick: () => {},
|
||||
onFilterAdded: () => {},
|
||||
ownFocus: false,
|
||||
showTooltip: true,
|
||||
value: aggData.key as string,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
})
|
||||
.flat() || [],
|
||||
};
|
||||
return result;
|
||||
}, [data?.pages, getFilterForValueButton, dataViewId, filterManager, getFilterOutValueButton]);
|
||||
|
||||
const copyToClipboardButtons = useMemo((): CopyButtons => {
|
||||
const result: CopyButtons = {
|
||||
copyButtons:
|
||||
data?.pages
|
||||
?.map((aggsData) => {
|
||||
return aggsData?.buckets.map((aggData) => {
|
||||
return getCopyButton({
|
||||
field: CONTAINER_IMAGE_NAME,
|
||||
size: 'xs',
|
||||
onClick: () => {},
|
||||
ownFocus: false,
|
||||
showTooltip: true,
|
||||
value: aggData.key as string,
|
||||
});
|
||||
});
|
||||
})
|
||||
.flat() || [],
|
||||
};
|
||||
return result;
|
||||
}, [data, getCopyButton]);
|
||||
|
||||
const containerNameArray = useMemo((): ContainerNameArrayDataValue[] => {
|
||||
return data
|
||||
? data?.pages
|
||||
?.map((aggsData) => {
|
||||
return aggsData?.buckets.map((aggData) => {
|
||||
return {
|
||||
name: aggData.key as string,
|
||||
count: addCommasToNumber(aggData.count_by_aggs?.value ?? 0),
|
||||
};
|
||||
});
|
||||
})
|
||||
.flat()
|
||||
: [];
|
||||
}, [data]);
|
||||
|
||||
const columns = useMemo((): Array<EuiBasicTableColumn<ContainerNameArrayDataValue>> => {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
name: CONTAINER_NAME_SESSION,
|
||||
'data-test-subj': NAME_COLUMN_TEST_ID,
|
||||
render: (name: string) => {
|
||||
const indexHelper = containerNameArray.findIndex((obj) => {
|
||||
return obj.name === name;
|
||||
});
|
||||
return (
|
||||
<ContainerNameRow
|
||||
name={name}
|
||||
filterButtonIn={filterButtons.filterForButtons[indexHelper]}
|
||||
filterButtonOut={filterButtons.filterOutButtons[indexHelper]}
|
||||
copyToClipboardButton={copyToClipboardButtons.copyButtons[indexHelper]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
align: 'left',
|
||||
width: '67%',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
name: CONTAINER_NAME_SESSION_COUNT_COLUMN,
|
||||
width: '33%',
|
||||
'data-test-subj': COUNT_COLUMN_TEST_ID,
|
||||
render: (count: number) => {
|
||||
return <span css={styles.countValue}>{count}</span>;
|
||||
},
|
||||
sortable: true,
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
}, [
|
||||
filterButtons.filterForButtons,
|
||||
filterButtons.filterOutButtons,
|
||||
copyToClipboardButtons.copyButtons,
|
||||
containerNameArray,
|
||||
styles,
|
||||
]);
|
||||
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
useScroll({
|
||||
div: scrollerRef.current,
|
||||
handler: (pos: number, endReached: boolean) => {
|
||||
if (!isFetchingNextPage && endReached) {
|
||||
fetchNextPage();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const cellProps = useMemo(() => {
|
||||
return {
|
||||
css: styles.cellPad,
|
||||
};
|
||||
}, [styles.cellPad]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test-subj={CONTAINER_NAME_TABLE_TEST_ID}
|
||||
className="eui-yScroll"
|
||||
css={styles.container}
|
||||
ref={scrollerRef}
|
||||
>
|
||||
{isLoading && (
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
color="accent"
|
||||
position="absolute"
|
||||
data-test-subj={LOADING_TEST_ID}
|
||||
/>
|
||||
)}
|
||||
<EuiBasicTable
|
||||
aria-label={CONTAINER_NAME_SESSION_ARIA_LABEL}
|
||||
items={containerNameArray}
|
||||
columns={columns}
|
||||
sorting={sorting}
|
||||
onChange={onTableChange}
|
||||
cellProps={cellProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { CSSObject } from '@emotion/react';
|
||||
import { transparentize } from '@elastic/eui';
|
||||
import { useEuiTheme } from '../../hooks';
|
||||
|
||||
export const useStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const cached = useMemo(() => {
|
||||
const { size, colors } = euiTheme;
|
||||
|
||||
const container: CSSObject = {
|
||||
paddingTop: size.s,
|
||||
paddingBottom: size.s,
|
||||
paddingRight: size.base,
|
||||
paddingLeft: size.base,
|
||||
border: euiTheme.border.thin,
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
overflow: 'auto',
|
||||
height: '228px',
|
||||
position: 'relative',
|
||||
marginBottom: size.l,
|
||||
};
|
||||
|
||||
const dataInfo: CSSObject = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: size.base,
|
||||
position: 'relative',
|
||||
};
|
||||
|
||||
const filters: CSSObject = {
|
||||
marginLeft: size.s,
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
backgroundColor: colors.emptyShade,
|
||||
borderRadius: euiTheme.border.radius.small,
|
||||
border: euiTheme.border.thin,
|
||||
bottom: '-25px',
|
||||
boxShadow: `0 ${size.xs} ${size.xs} ${transparentize(euiTheme.colors.shadow, 0.04)}`,
|
||||
display: 'flex',
|
||||
zIndex: 1,
|
||||
};
|
||||
|
||||
const countValue: CSSObject = {
|
||||
fontSize: size.m,
|
||||
};
|
||||
|
||||
const truncate: CSSObject = {
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
};
|
||||
|
||||
const flexWidth: CSSObject = {
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
const cellPad: CSSObject = {
|
||||
paddingBottom: '5px',
|
||||
paddingTop: '5px',
|
||||
};
|
||||
|
||||
return {
|
||||
container,
|
||||
dataInfo,
|
||||
filters,
|
||||
countValue,
|
||||
truncate,
|
||||
flexWidth,
|
||||
cellPad,
|
||||
};
|
||||
}, [euiTheme]);
|
||||
|
||||
return cached;
|
||||
};
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { addResourceTypeToFilterQuery, numberFormatter } from './helpers';
|
||||
|
||||
const TEST_DATA_ARRAY: number[] = [
|
||||
32,
|
||||
2200,
|
||||
999232,
|
||||
1310000,
|
||||
999999999,
|
||||
999999999999,
|
||||
1230000000000,
|
||||
Infinity,
|
||||
NaN,
|
||||
-1,
|
||||
-Infinity,
|
||||
1e15 - 1,
|
||||
];
|
||||
|
||||
const TEST_QUERY = `{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}`;
|
||||
|
||||
const RESULT_QUERY_NODE =
|
||||
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"orchestrator.resource.type":"node"}}]}}],"should":[],"must_not":[]}}';
|
||||
const RESULT_QUERY_POD =
|
||||
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"orchestrator.resource.type":"pod"}}]}}],"should":[],"must_not":[]}}';
|
||||
|
||||
describe('Testing Helper functions', () => {
|
||||
it('Testing numberFormatter helper function', () => {
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[0])).toBe('32');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[1])).toBe('2K');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[2])).toBe('999K');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[3])).toBe('1.3M');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[4])).toBe('999M');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[5])).toBe('999B');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[6])).toBe('1.2T');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[7])).toBe('NaN');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[8])).toBe('NaN');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[9])).toBe('NaN');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[10])).toBe('NaN');
|
||||
expect(numberFormatter(TEST_DATA_ARRAY[11])).toBe('999T');
|
||||
});
|
||||
|
||||
it('Testing addResourceTypeToFilterQuery helper function', () => {
|
||||
expect(addResourceTypeToFilterQuery(TEST_QUERY, 'node')).toBe(RESULT_QUERY_NODE);
|
||||
expect(addResourceTypeToFilterQuery(TEST_QUERY, 'pod')).toBe(RESULT_QUERY_POD);
|
||||
});
|
||||
});
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DEFAULT_FILTER_QUERY } from '../../../common/constants';
|
||||
import { QueryDslQueryContainerBool } from '../../types';
|
||||
|
||||
export const addResourceTypeToFilterQuery = (
|
||||
filterQuery: string | undefined,
|
||||
resourceType: 'node' | 'pod'
|
||||
) => {
|
||||
let validFilterQuery = DEFAULT_FILTER_QUERY;
|
||||
|
||||
try {
|
||||
const parsedFilterQuery: QueryDslQueryContainerBool = JSON.parse(filterQuery || '{}');
|
||||
if (!(parsedFilterQuery?.bool?.filter && Array.isArray(parsedFilterQuery.bool.filter))) {
|
||||
throw new Error('Invalid filter query');
|
||||
}
|
||||
parsedFilterQuery.bool.filter.push({
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match_phrase: {
|
||||
'orchestrator.resource.type': resourceType,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
validFilterQuery = JSON.stringify(parsedFilterQuery);
|
||||
} catch {
|
||||
// no-op since validFilterQuery is initialized to be DEFAULT_FILTER_QUERY
|
||||
}
|
||||
|
||||
return validFilterQuery;
|
||||
};
|
||||
|
||||
export const numberFormatter = (num: number) => {
|
||||
if (Number.isFinite(num) && num >= 0) {
|
||||
if (num >= 1e15 - 1) {
|
||||
const newNum = Math.floor(num / 1e12) * 1e12;
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
// @ts-ignore
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(newNum);
|
||||
}
|
||||
// Trillion
|
||||
if (num >= 1e12 - 1) {
|
||||
const newNum = Math.floor(num / 1e9) * 1e9;
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
// @ts-ignore
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(newNum);
|
||||
}
|
||||
// Billion
|
||||
if (num >= 1e9 - 1) {
|
||||
const newNum = Math.floor(num / 1e6) * 1e6;
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
// @ts-ignore
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(newNum);
|
||||
}
|
||||
// Hundreds Thousands
|
||||
if (num >= 1e6 - 1) {
|
||||
const newNum = Math.floor(num / 1000) * 1000;
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
// @ts-ignore
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(newNum);
|
||||
}
|
||||
// Thousands
|
||||
if (num >= 1e3 - 1) {
|
||||
const newNum = Math.floor(num / 1000) * 1000;
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
// @ts-ignore
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(newNum);
|
||||
}
|
||||
|
||||
if (num < 1e3) {
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
// @ts-ignore
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(num);
|
||||
}
|
||||
return 'NaN';
|
||||
};
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
QUERY_KEY_COUNT_WIDGET,
|
||||
COUNT_ROUTE,
|
||||
CURRENT_API_VERSION,
|
||||
} from '../../../common/constants';
|
||||
|
||||
export const useFetchCountWidgetData = (
|
||||
widgetKey: string,
|
||||
filterQuery: string,
|
||||
groupedBy: string,
|
||||
index?: string
|
||||
) => {
|
||||
const { http } = useKibana<CoreStart>().services;
|
||||
const cachingKeys = [QUERY_KEY_COUNT_WIDGET, widgetKey, filterQuery, groupedBy, index];
|
||||
|
||||
const query = useInfiniteQuery(
|
||||
cachingKeys,
|
||||
async () => {
|
||||
const res = await http.get<number>(COUNT_ROUTE, {
|
||||
version: CURRENT_API_VERSION,
|
||||
query: {
|
||||
query: filterQuery,
|
||||
field: groupedBy,
|
||||
index,
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
}
|
||||
);
|
||||
|
||||
return query;
|
||||
};
|
|
@ -1,146 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { GlobalFilter } from '../../types';
|
||||
import { CountWidget, LOADING_TEST_ID, TOOLTIP_TEST_ID, VALUE_TEST_ID } from '.';
|
||||
import { useFetchCountWidgetData } from './hooks';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
|
||||
const TITLE = 'Count Widget Title';
|
||||
const GLOBAL_FILTER: GlobalFilter = {
|
||||
endDate: '2022-06-15T14:15:25.777Z',
|
||||
filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}',
|
||||
startDate: '2022-05-15T14:15:25.777Z',
|
||||
};
|
||||
|
||||
const MOCK_DATA_NORMAL = {
|
||||
pages: [12],
|
||||
};
|
||||
|
||||
const MOCK_DATA_MILLION = {
|
||||
pages: [1210000],
|
||||
};
|
||||
|
||||
const MOCK_DATA_THOUSAND = {
|
||||
pages: [5236],
|
||||
};
|
||||
|
||||
const MOCK_DATA_CLOSE_TO_MILLION = {
|
||||
pages: [999999],
|
||||
};
|
||||
|
||||
jest.mock('../../hooks/use_filter', () => ({
|
||||
useSetFilter: () => ({
|
||||
getFilterForValueButton: jest.fn(),
|
||||
getFilterOutValueButton: jest.fn(),
|
||||
filterManager: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('./hooks');
|
||||
const mockUseFetchData = useFetchCountWidgetData as jest.Mock;
|
||||
|
||||
describe('CountWidget component', () => {
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
const mockedContext = createAppRootMockRenderer();
|
||||
const render: () => ReturnType<AppContextTestRender['render']> = () =>
|
||||
(renderResult = mockedContext.render(
|
||||
<CountWidget
|
||||
title={TITLE}
|
||||
globalFilter={GLOBAL_FILTER}
|
||||
widgetKey="CountContainerImagesWidgets"
|
||||
groupedBy={'container.image.name'}
|
||||
/>
|
||||
));
|
||||
|
||||
describe('When PercentWidget is mounted', () => {
|
||||
describe('with small amount of data (less than 1000)', () => {
|
||||
beforeEach(() => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: MOCK_DATA_NORMAL,
|
||||
isLoading: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should show title and count numbers correctly', async () => {
|
||||
render();
|
||||
|
||||
expect(renderResult.getByText(TITLE)).toBeVisible();
|
||||
expect(renderResult.getByText('12')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with moderate amount of data (more than 1000 less than 1 million)', () => {
|
||||
beforeEach(() => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: MOCK_DATA_THOUSAND,
|
||||
isLoading: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should show title and count numbers (formatted thousands with comma)correctly', async () => {
|
||||
render();
|
||||
|
||||
expect(renderResult.getByText(TITLE)).toBeVisible();
|
||||
expect(renderResult.getByText('5K')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with huge amount of data (more than 1 million)', () => {
|
||||
it('should show title and count numbers (formatted remove the zeroes and add M) correctly', async () => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: MOCK_DATA_MILLION,
|
||||
isLoading: false,
|
||||
}));
|
||||
render();
|
||||
|
||||
expect(renderResult.getByText(TITLE)).toBeVisible();
|
||||
expect(renderResult.getByText('1.2M')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with huge amount of data (Very close to 1 million)', () => {
|
||||
it('should show title and count numbers (formatted remove the zeroes and add K) correctly', async () => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: MOCK_DATA_CLOSE_TO_MILLION,
|
||||
isLoading: false,
|
||||
}));
|
||||
render();
|
||||
|
||||
expect(renderResult.getByText(TITLE)).toBeVisible();
|
||||
expect(renderResult.getByText('999K')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When data is loading', () => {
|
||||
it('should show the loading icon', async () => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: MOCK_DATA_MILLION,
|
||||
isLoading: true,
|
||||
}));
|
||||
render();
|
||||
|
||||
expect(renderResult.getAllByTestId(LOADING_TEST_ID)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Testing Tooltips', () => {
|
||||
it('Tooltips show the real count value (not formatted)', async () => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: MOCK_DATA_THOUSAND,
|
||||
isLoading: false,
|
||||
}));
|
||||
render();
|
||||
fireEvent.mouseOver(renderResult.getByTestId(VALUE_TEST_ID));
|
||||
await waitFor(() => renderResult.getByTestId(TOOLTIP_TEST_ID));
|
||||
expect(renderResult.queryByText('5,236')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiText, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
|
||||
import { useStyles } from './styles';
|
||||
import type { IndexPattern, GlobalFilter } from '../../types';
|
||||
import { addCommasToNumber } from '../../utils/add_commas_to_number';
|
||||
import { addTimerangeAndDefaultFilterToQuery } from '../../utils/add_timerange_and_default_filter_to_query';
|
||||
import { useFetchCountWidgetData } from './hooks';
|
||||
import { addResourceTypeToFilterQuery, numberFormatter } from './helpers';
|
||||
import { COUNT_WIDGET_KEY_PODS } from '../../../common/constants';
|
||||
|
||||
export const LOADING_TEST_ID = 'kubernetesSecurity:countWidgetLoading';
|
||||
export const TOOLTIP_TEST_ID = 'kubernetesSecurity:countWidgetTooltip';
|
||||
export const VALUE_TEST_ID = 'kubernetesSecurity:countWidgetValue';
|
||||
|
||||
export interface CountWidgetDeps {
|
||||
title: string;
|
||||
widgetKey: string;
|
||||
indexPattern?: IndexPattern;
|
||||
globalFilter: GlobalFilter;
|
||||
groupedBy: string;
|
||||
}
|
||||
|
||||
export const CountWidget = ({
|
||||
title,
|
||||
widgetKey,
|
||||
indexPattern,
|
||||
globalFilter,
|
||||
groupedBy,
|
||||
}: CountWidgetDeps) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const filterQueryWithTimeRange = useMemo(() => {
|
||||
let globalFilterModified = globalFilter.filterQuery;
|
||||
|
||||
if (widgetKey === COUNT_WIDGET_KEY_PODS) {
|
||||
globalFilterModified = addResourceTypeToFilterQuery(globalFilter.filterQuery, 'pod');
|
||||
}
|
||||
return addTimerangeAndDefaultFilterToQuery(
|
||||
globalFilterModified,
|
||||
globalFilter.startDate,
|
||||
globalFilter.endDate
|
||||
);
|
||||
}, [globalFilter.filterQuery, globalFilter.startDate, globalFilter.endDate, widgetKey]);
|
||||
|
||||
const { data, isLoading } = useFetchCountWidgetData(
|
||||
widgetKey,
|
||||
filterQueryWithTimeRange,
|
||||
groupedBy,
|
||||
indexPattern?.title
|
||||
);
|
||||
|
||||
const countValue = useMemo((): number => {
|
||||
return data ? data?.pages[0] : 0;
|
||||
}, [data]);
|
||||
|
||||
const formattedNumber = useMemo((): string => {
|
||||
return numberFormatter(countValue);
|
||||
}, [countValue]);
|
||||
|
||||
return (
|
||||
<div css={styles.container}>
|
||||
<div css={styles.title}>{title}</div>
|
||||
<EuiToolTip
|
||||
content={isLoading ? null : addCommasToNumber(countValue)}
|
||||
data-test-subj={TOOLTIP_TEST_ID}
|
||||
aria-label="Info"
|
||||
position="top"
|
||||
>
|
||||
<EuiText css={styles.dataInfo} data-test-subj={VALUE_TEST_ID}>
|
||||
{isLoading ? (
|
||||
<EuiLoadingSpinner size="l" data-test-subj={LOADING_TEST_ID} />
|
||||
) : (
|
||||
formattedNumber
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { CSSObject } from '@emotion/react';
|
||||
import { useEuiTheme } from '../../hooks';
|
||||
|
||||
export const useStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const cached = useMemo(() => {
|
||||
const { size, font, border } = euiTheme;
|
||||
|
||||
const container: CSSObject = {
|
||||
padding: size.base,
|
||||
border: border.thin,
|
||||
borderRadius: border.radius.medium,
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
const title: CSSObject = {
|
||||
marginBottom: size.s,
|
||||
fontSize: size.m,
|
||||
fontWeight: font.weight.bold,
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
|
||||
const dataInfo: CSSObject = {
|
||||
fontSize: `calc(${size.l} - ${size.xxs})`,
|
||||
lineHeight: size.l,
|
||||
fontWeight: font.weight.bold,
|
||||
};
|
||||
|
||||
const dataValue: CSSObject = {
|
||||
fontWeight: font.weight.semiBold,
|
||||
marginLeft: 'auto',
|
||||
};
|
||||
|
||||
const filters: CSSObject = {
|
||||
marginLeft: size.s,
|
||||
};
|
||||
|
||||
const loadingSpinner: CSSObject = {
|
||||
alignItems: 'center',
|
||||
margin: `${size.xs} auto ${size.xl} auto`,
|
||||
};
|
||||
|
||||
return {
|
||||
container,
|
||||
title,
|
||||
dataInfo,
|
||||
dataValue,
|
||||
filters,
|
||||
loadingSpinner,
|
||||
};
|
||||
}, [euiTheme]);
|
||||
|
||||
return cached;
|
||||
};
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import { MemoryRouterProps } from 'react-router';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { KubernetesSecurityRoutes } from '.';
|
||||
import { createAppRootMockRenderer } from '../../test';
|
||||
|
||||
jest.mock('../percent_widget', () => ({
|
||||
PercentWidget: () => <div>{'Mock percent widget'}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks/use_last_updated', () => ({
|
||||
useLastUpdated: () => <div>{'Mock updated now'}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('../count_widget', () => ({
|
||||
CountWidget: () => <div>{'Mock count widget'}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('../container_name_widget', () => ({
|
||||
ContainerNameWidget: () => <div>{'Mock Container Name widget'}</div>,
|
||||
}));
|
||||
|
||||
const dataViewId = 'dataViewId';
|
||||
|
||||
const renderWithRouter = (
|
||||
initialEntries: MemoryRouterProps['initialEntries'] = ['/kubernetes']
|
||||
) => {
|
||||
const useGlobalFullScreen = jest.fn();
|
||||
useGlobalFullScreen.mockImplementation(() => {
|
||||
return { globalFullScreen: false };
|
||||
});
|
||||
const useSourcererDataView = jest.fn();
|
||||
useSourcererDataView.mockImplementation(() => {
|
||||
return {
|
||||
indexPattern: {
|
||||
fields: [
|
||||
{
|
||||
aggregatable: false,
|
||||
esTypes: [],
|
||||
name: '_id',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
title: '.mock-index-pattern',
|
||||
},
|
||||
};
|
||||
});
|
||||
const mockedContext = createAppRootMockRenderer();
|
||||
return mockedContext.render(
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<KubernetesSecurityRoutes
|
||||
filter={<div>{'Mock filters'}</div>}
|
||||
globalFilter={{
|
||||
filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}',
|
||||
startDate: '2022-03-08T18:52:15.532Z',
|
||||
endDate: '2022-06-09T17:52:15.532Z',
|
||||
}}
|
||||
renderSessionsView={jest.fn()}
|
||||
dataViewId={dataViewId}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Kubernetes security routes', () => {
|
||||
it('navigates to the kubernetes page', () => {
|
||||
renderWithRouter();
|
||||
expect(screen.getAllByText('Mock count widget')).toHaveLength(5);
|
||||
expect(screen.getAllByText('Mock percent widget')).toHaveLength(2);
|
||||
expect(screen.getAllByText('Mock updated now')).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -1,288 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { Routes, Route } from '@kbn/shared-ux-router';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import {
|
||||
KUBERNETES_PATH,
|
||||
KUBERNETES_TITLE,
|
||||
LOCAL_STORAGE_HIDE_WIDGETS_KEY,
|
||||
ENTRY_LEADER_INTERACTIVE,
|
||||
ENTRY_LEADER_USER_ID,
|
||||
ENTRY_LEADER_ENTITY_ID,
|
||||
ORCHESTRATOR_CLUSTER_ID,
|
||||
ORCHESTRATOR_NAMESPACE,
|
||||
ORCHESTRATOR_RESOURCE_ID,
|
||||
CONTAINER_IMAGE_NAME,
|
||||
CLOUD_INSTANCE_NAME,
|
||||
COUNT_WIDGET_KEY_CLUSTERS,
|
||||
COUNT_WIDGET_KEY_NAMESPACE,
|
||||
COUNT_WIDGET_KEY_NODES,
|
||||
COUNT_WIDGET_KEY_CONTAINER_IMAGES,
|
||||
} from '../../../common/constants';
|
||||
import { PercentWidget } from '../percent_widget';
|
||||
import { CountWidget } from '../count_widget';
|
||||
import { KubernetesSecurityDeps } from '../../types';
|
||||
import { AggregateResult } from '../../../common/types';
|
||||
import { useLastUpdated } from '../../hooks';
|
||||
import { useStyles } from './styles';
|
||||
import { TreeViewContainer } from '../tree_view_container';
|
||||
import { ChartsToggle } from '../charts_toggle';
|
||||
import {
|
||||
COUNT_WIDGET_CLUSTERS,
|
||||
COUNT_WIDGET_NAMESPACE,
|
||||
COUNT_WIDGET_NODES,
|
||||
COUNT_WIDGET_PODS,
|
||||
COUNT_WIDGET_CONTAINER_IMAGES,
|
||||
} from '../../../common/translations';
|
||||
import { ContainerNameWidget } from '../container_name_widget';
|
||||
|
||||
const KubernetesSecurityRoutesComponent = ({
|
||||
dataViewId,
|
||||
filter,
|
||||
indexPattern,
|
||||
globalFilter,
|
||||
renderSessionsView,
|
||||
}: KubernetesSecurityDeps) => {
|
||||
const [shouldHideCharts, setShouldHideCharts] = useLocalStorage(
|
||||
LOCAL_STORAGE_HIDE_WIDGETS_KEY,
|
||||
false
|
||||
);
|
||||
const styles = useStyles();
|
||||
const lastUpdated = useLastUpdated(globalFilter);
|
||||
const onReduceInteractiveAggs = useCallback(
|
||||
(result: AggregateResult): Record<string, number> =>
|
||||
result.buckets.reduce((groupedByKeyValue, aggregate) => {
|
||||
groupedByKeyValue[aggregate.key_as_string || (aggregate.key.toString() as string)] =
|
||||
aggregate.count_by_aggs?.value ?? 0;
|
||||
return groupedByKeyValue;
|
||||
}, {} as Record<string, number>),
|
||||
[]
|
||||
);
|
||||
|
||||
const onReduceRootAggs = useCallback(
|
||||
(result: AggregateResult): Record<string, number> =>
|
||||
result.buckets.reduce((groupedByKeyValue, aggregate) => {
|
||||
if (aggregate.key.toString() === '0') {
|
||||
groupedByKeyValue[aggregate.key] = aggregate.count_by_aggs?.value ?? 0;
|
||||
} else {
|
||||
groupedByKeyValue.nonRoot =
|
||||
(groupedByKeyValue.nonRoot || 0) + (aggregate.count_by_aggs?.value ?? 0);
|
||||
}
|
||||
return groupedByKeyValue;
|
||||
}, {} as Record<string, number>),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleToggleHideCharts = useCallback(() => {
|
||||
setShouldHideCharts(!shouldHideCharts);
|
||||
}, [setShouldHideCharts, shouldHideCharts]);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route strict exact path={KUBERNETES_PATH}>
|
||||
{filter}
|
||||
<EuiFlexGroup gutterSize="none" css={styles.titleSection}>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="l">
|
||||
<h1 css={styles.titleText}>{KUBERNETES_TITLE}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={styles.titleActions}>
|
||||
<div css={styles.updatedAt}>{lastUpdated}</div>
|
||||
<ChartsToggle
|
||||
shouldHideCharts={shouldHideCharts}
|
||||
handleToggleHideCharts={handleToggleHideCharts}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{!shouldHideCharts && (
|
||||
<>
|
||||
<EuiFlexGroup css={styles.widgetsGroup}>
|
||||
<EuiFlexItem css={styles.leftWidgetsGroup}>
|
||||
<EuiFlexGroup css={styles.countWidgetsGroup}>
|
||||
<EuiFlexItem>
|
||||
<CountWidget
|
||||
title={COUNT_WIDGET_CLUSTERS}
|
||||
indexPattern={indexPattern}
|
||||
globalFilter={globalFilter}
|
||||
widgetKey={COUNT_WIDGET_KEY_CLUSTERS}
|
||||
groupedBy={ORCHESTRATOR_CLUSTER_ID}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CountWidget
|
||||
title={COUNT_WIDGET_NAMESPACE}
|
||||
indexPattern={indexPattern}
|
||||
globalFilter={globalFilter}
|
||||
widgetKey={COUNT_WIDGET_KEY_NAMESPACE}
|
||||
groupedBy={ORCHESTRATOR_NAMESPACE}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CountWidget
|
||||
title={COUNT_WIDGET_NODES}
|
||||
indexPattern={indexPattern}
|
||||
globalFilter={globalFilter}
|
||||
widgetKey={COUNT_WIDGET_KEY_NODES}
|
||||
groupedBy={CLOUD_INSTANCE_NAME}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CountWidget
|
||||
title={COUNT_WIDGET_PODS}
|
||||
indexPattern={indexPattern}
|
||||
globalFilter={globalFilter}
|
||||
widgetKey={COUNT_WIDGET_KEY_NODES}
|
||||
groupedBy={ORCHESTRATOR_RESOURCE_ID}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CountWidget
|
||||
title={COUNT_WIDGET_CONTAINER_IMAGES}
|
||||
indexPattern={indexPattern}
|
||||
globalFilter={globalFilter}
|
||||
widgetKey={COUNT_WIDGET_KEY_CONTAINER_IMAGES}
|
||||
groupedBy={CONTAINER_IMAGE_NAME}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup css={styles.widgetsBottomSpacing}>
|
||||
<EuiFlexItem>
|
||||
<PercentWidget
|
||||
dataViewId={dataViewId}
|
||||
title={
|
||||
<>
|
||||
<EuiText size="xs" css={styles.percentageChartTitle}>
|
||||
<FormattedMessage
|
||||
id="xpack.kubernetesSecurity.sessionChart.title"
|
||||
defaultMessage="Session interactivity"
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiIconTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.kubernetesSecurity.sessionChart.tooltip"
|
||||
defaultMessage="Interactive sessions have a controlling terminal and often
|
||||
imply that a human is entering the commands."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
widgetKey="sessionsPercentage"
|
||||
indexPattern={indexPattern}
|
||||
globalFilter={globalFilter}
|
||||
dataValueMap={{
|
||||
true: {
|
||||
name: i18n.translate(
|
||||
'xpack.kubernetesSecurity.sessionChart.interactive',
|
||||
{
|
||||
defaultMessage: 'Interactive',
|
||||
}
|
||||
),
|
||||
fieldName: ENTRY_LEADER_INTERACTIVE,
|
||||
color: euiThemeVars.euiColorVis0,
|
||||
},
|
||||
false: {
|
||||
name: i18n.translate(
|
||||
'xpack.kubernetesSecurity.sessionChart.nonInteractive',
|
||||
{
|
||||
defaultMessage: 'Non-interactive',
|
||||
}
|
||||
),
|
||||
fieldName: ENTRY_LEADER_INTERACTIVE,
|
||||
color: euiThemeVars.euiColorVis1,
|
||||
shouldHideFilter: true,
|
||||
},
|
||||
}}
|
||||
groupedBy={ENTRY_LEADER_INTERACTIVE}
|
||||
countBy={ENTRY_LEADER_ENTITY_ID}
|
||||
onReduce={onReduceInteractiveAggs}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PercentWidget
|
||||
title={
|
||||
<>
|
||||
<EuiText size="xs" css={styles.percentageChartTitle}>
|
||||
<FormattedMessage
|
||||
id="xpack.kubernetesSecurity.entryUserChart.title"
|
||||
defaultMessage="Entry session users"
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiIconTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.kubernetesSecurity.entryUserChart.tooltip"
|
||||
defaultMessage="The entry session user is the initial Linux user associated
|
||||
with the session. This user may be set from authentication of a remote
|
||||
login or automatically for service sessions started by init."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
widgetKey="rootLoginPercentage"
|
||||
indexPattern={indexPattern}
|
||||
globalFilter={globalFilter}
|
||||
dataValueMap={{
|
||||
'0': {
|
||||
name: i18n.translate('xpack.kubernetesSecurity.entryUserChart.root', {
|
||||
defaultMessage: 'Root',
|
||||
}),
|
||||
fieldName: ENTRY_LEADER_USER_ID,
|
||||
color: euiThemeVars.euiColorVis2,
|
||||
},
|
||||
nonRoot: {
|
||||
name: i18n.translate('xpack.kubernetesSecurity.entryUserChart.nonRoot', {
|
||||
defaultMessage: 'Non-root',
|
||||
}),
|
||||
fieldName: ENTRY_LEADER_USER_ID,
|
||||
color: euiThemeVars.euiColorVis3,
|
||||
shouldHideFilter: true,
|
||||
},
|
||||
}}
|
||||
dataViewId={dataViewId}
|
||||
groupedBy={ENTRY_LEADER_USER_ID}
|
||||
countBy={ENTRY_LEADER_ENTITY_ID}
|
||||
onReduce={onReduceRootAggs}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={styles.rightWidgetsGroup}>
|
||||
<ContainerNameWidget
|
||||
dataViewId={dataViewId}
|
||||
widgetKey="containerNameSessions"
|
||||
indexPattern={indexPattern}
|
||||
globalFilter={globalFilter}
|
||||
groupedBy={CONTAINER_IMAGE_NAME}
|
||||
countBy={ENTRY_LEADER_ENTITY_ID}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
<TreeViewContainer
|
||||
globalFilter={globalFilter}
|
||||
renderSessionsView={renderSessionsView}
|
||||
indexPattern={indexPattern}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export const KubernetesSecurityRoutes = React.memo(KubernetesSecurityRoutesComponent);
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { KubernetesSecurityRoutes as default };
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import type { CSSObject } from '@emotion/react';
|
||||
import { useEuiTheme } from '../../hooks';
|
||||
|
||||
export const useStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const cached = useMemo(() => {
|
||||
const { size, font } = euiTheme;
|
||||
|
||||
const titleSection: CSSObject = {
|
||||
marginBottom: size.l,
|
||||
};
|
||||
|
||||
const titleText: CSSObject = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const titleActions: CSSObject = {
|
||||
marginLeft: 'auto',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const updatedAt: CSSObject = {
|
||||
marginRight: size.m,
|
||||
};
|
||||
|
||||
const widgetBadge: CSSObject = {
|
||||
position: 'absolute',
|
||||
bottom: size.base,
|
||||
left: size.base,
|
||||
width: `calc(100% - ${size.xl})`,
|
||||
fontSize: size.m,
|
||||
lineHeight: '18px',
|
||||
padding: `${size.xs} ${size.s}`,
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
const treeViewContainer: CSSObject = {
|
||||
position: 'relative',
|
||||
border: euiTheme.border.thin,
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
padding: size.base,
|
||||
height: '500px',
|
||||
};
|
||||
|
||||
const widgetsBottomSpacing: CSSObject = {
|
||||
marginBottom: size.m,
|
||||
};
|
||||
|
||||
const countWidgetsGroup: CSSObject = {
|
||||
...widgetsBottomSpacing,
|
||||
flexWrap: 'wrap',
|
||||
[`@media (max-width:${euiTheme.breakpoint.xl}px)`]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
};
|
||||
|
||||
const leftWidgetsGroup: CSSObject = {
|
||||
[`@media (max-width:${euiTheme.breakpoint.xl}px)`]: {
|
||||
marginBottom: '0 !important',
|
||||
},
|
||||
minWidth: `calc(70% - ${size.xxxl})`,
|
||||
};
|
||||
|
||||
const rightWidgetsGroup: CSSObject = {
|
||||
[`@media (max-width:${euiTheme.breakpoint.xl}px)`]: {
|
||||
marginTop: '0 !important',
|
||||
},
|
||||
minWidth: '30%',
|
||||
};
|
||||
|
||||
const percentageChartTitle: CSSObject = {
|
||||
marginRight: size.xs,
|
||||
display: 'inline',
|
||||
fontWeight: font.weight.bold,
|
||||
};
|
||||
|
||||
const widgetsGroup: CSSObject = {
|
||||
[`@media (max-width:${euiTheme.breakpoint.xl}px)`]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
titleSection,
|
||||
titleText,
|
||||
titleActions,
|
||||
updatedAt,
|
||||
widgetBadge,
|
||||
treeViewContainer,
|
||||
countWidgetsGroup,
|
||||
leftWidgetsGroup,
|
||||
rightWidgetsGroup,
|
||||
widgetsBottomSpacing,
|
||||
percentageChartTitle,
|
||||
widgetsGroup,
|
||||
};
|
||||
}, [euiTheme]);
|
||||
|
||||
return cached;
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
QUERY_KEY_PERCENT_WIDGET,
|
||||
AGGREGATE_ROUTE,
|
||||
CURRENT_API_VERSION,
|
||||
} from '../../../common/constants';
|
||||
import { AggregateResult } from '../../../common/types';
|
||||
|
||||
export const useFetchPercentWidgetData = (
|
||||
onReduce: (result: AggregateResult) => Record<string, number>,
|
||||
filterQuery: string,
|
||||
widgetKey: string,
|
||||
groupBy: string,
|
||||
countBy?: string,
|
||||
index?: string
|
||||
) => {
|
||||
const { http } = useKibana<CoreStart>().services;
|
||||
const cachingKeys = [QUERY_KEY_PERCENT_WIDGET, widgetKey, filterQuery, groupBy, countBy, index];
|
||||
const query = useQuery(cachingKeys, async (): Promise<Record<string, number>> => {
|
||||
const res = await http.get<AggregateResult>(AGGREGATE_ROUTE, {
|
||||
version: CURRENT_API_VERSION,
|
||||
query: {
|
||||
query: filterQuery,
|
||||
groupBy,
|
||||
countBy,
|
||||
page: 0,
|
||||
index,
|
||||
},
|
||||
});
|
||||
|
||||
return onReduce(res);
|
||||
});
|
||||
|
||||
return query;
|
||||
};
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ENTRY_LEADER_INTERACTIVE } from '../../../common/constants';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { GlobalFilter } from '../../types';
|
||||
import { PercentWidget, LOADING_TEST_ID, PERCENT_DATA_TEST_ID } from '.';
|
||||
import { useFetchPercentWidgetData } from './hooks';
|
||||
|
||||
const MOCK_DATA: Record<string, number> = {
|
||||
false: 47,
|
||||
true: 1,
|
||||
};
|
||||
const TITLE = 'Percent Widget Title';
|
||||
const GLOBAL_FILTER: GlobalFilter = {
|
||||
endDate: '2022-06-15T14:15:25.777Z',
|
||||
filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}',
|
||||
startDate: '2022-05-15T14:15:25.777Z',
|
||||
};
|
||||
const DATA_VALUE_MAP = {
|
||||
true: {
|
||||
name: 'Interactive',
|
||||
fieldName: ENTRY_LEADER_INTERACTIVE,
|
||||
color: 'red',
|
||||
},
|
||||
false: {
|
||||
name: 'Non-interactive',
|
||||
fieldName: ENTRY_LEADER_INTERACTIVE,
|
||||
color: 'blue',
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_DATA_VIEW_ID = 'dataViewId';
|
||||
|
||||
jest.mock('../../hooks/use_filter', () => ({
|
||||
useSetFilter: () => ({
|
||||
getFilterForValueButton: jest.fn(),
|
||||
getFilterOutValueButton: jest.fn(),
|
||||
filterManager: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('./hooks');
|
||||
const mockUseFetchData = useFetchPercentWidgetData as jest.Mock;
|
||||
|
||||
describe('PercentWidget component', () => {
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
const mockedContext = createAppRootMockRenderer();
|
||||
const render: () => ReturnType<AppContextTestRender['render']> = () =>
|
||||
(renderResult = mockedContext.render(
|
||||
<PercentWidget
|
||||
title={TITLE}
|
||||
dataValueMap={DATA_VALUE_MAP}
|
||||
dataViewId={MOCK_DATA_VIEW_ID}
|
||||
widgetKey="percentWidget"
|
||||
globalFilter={GLOBAL_FILTER}
|
||||
groupedBy={ENTRY_LEADER_INTERACTIVE}
|
||||
onReduce={jest.fn()}
|
||||
/>
|
||||
));
|
||||
|
||||
describe('When PercentWidget is mounted', () => {
|
||||
describe('with data', () => {
|
||||
beforeEach(() => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: MOCK_DATA,
|
||||
isLoading: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should show title', async () => {
|
||||
render();
|
||||
|
||||
expect(renderResult.getByText(TITLE)).toBeVisible();
|
||||
});
|
||||
it('should show data value names and value', async () => {
|
||||
render();
|
||||
|
||||
expect(renderResult.getByText('Interactive')).toBeVisible();
|
||||
expect(renderResult.getByText('Non-interactive')).toBeVisible();
|
||||
expect(renderResult.queryByTestId(LOADING_TEST_ID)).toBeNull();
|
||||
expect(renderResult.getByText(47)).toBeVisible();
|
||||
expect(renderResult.getByText(1)).toBeVisible();
|
||||
});
|
||||
it('should show same number of data items as the number of records provided in dataValueMap', async () => {
|
||||
render();
|
||||
|
||||
expect(renderResult.getAllByTestId(PERCENT_DATA_TEST_ID)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without data ', () => {
|
||||
it('should show data value names and zeros as values when loading', async () => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
}));
|
||||
render();
|
||||
|
||||
expect(renderResult.getByText('Interactive')).toBeVisible();
|
||||
expect(renderResult.getByText('Non-interactive')).toBeVisible();
|
||||
expect(renderResult.getByTestId(LOADING_TEST_ID)).toBeVisible();
|
||||
expect(renderResult.getAllByText(0)).toHaveLength(2);
|
||||
});
|
||||
it('should show zeros as values if no data returned', async () => {
|
||||
mockUseFetchData.mockImplementation(() => ({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}));
|
||||
render();
|
||||
|
||||
expect(renderResult.getByText('Interactive')).toBeVisible();
|
||||
expect(renderResult.getByText('Non-interactive')).toBeVisible();
|
||||
expect(renderResult.getAllByText(0)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,165 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useMemo, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiText } from '@elastic/eui';
|
||||
import { useStyles } from './styles';
|
||||
import type { IndexPattern, GlobalFilter } from '../../types';
|
||||
import { useSetFilter } from '../../hooks';
|
||||
import { addTimerangeAndDefaultFilterToQuery } from '../../utils/add_timerange_and_default_filter_to_query';
|
||||
import { AggregateResult } from '../../../common/types';
|
||||
import { useFetchPercentWidgetData } from './hooks';
|
||||
|
||||
export const LOADING_TEST_ID = 'kubernetesSecurity:percentWidgetLoading';
|
||||
export const PERCENT_DATA_TEST_ID = 'kubernetesSecurity:percentWidgetData';
|
||||
|
||||
export interface PercenWidgetDataValueMap {
|
||||
name: string;
|
||||
fieldName: string;
|
||||
color: string;
|
||||
shouldHideFilter?: boolean;
|
||||
}
|
||||
|
||||
export interface PercentWidgetDeps {
|
||||
title: ReactNode;
|
||||
dataValueMap: Record<string, PercenWidgetDataValueMap>;
|
||||
widgetKey: string;
|
||||
indexPattern?: IndexPattern;
|
||||
globalFilter: GlobalFilter;
|
||||
groupedBy: string;
|
||||
countBy?: string;
|
||||
onReduce: (result: AggregateResult) => Record<string, number>;
|
||||
dataViewId?: string;
|
||||
}
|
||||
|
||||
interface FilterButtons {
|
||||
filterForButtons: ReactNode[];
|
||||
filterOutButtons: ReactNode[];
|
||||
}
|
||||
|
||||
export const PercentWidget = ({
|
||||
title,
|
||||
dataValueMap,
|
||||
widgetKey,
|
||||
indexPattern,
|
||||
globalFilter,
|
||||
groupedBy,
|
||||
countBy,
|
||||
onReduce,
|
||||
dataViewId,
|
||||
}: PercentWidgetDeps) => {
|
||||
const [hoveredFilter, setHoveredFilter] = useState<number | null>(null);
|
||||
const styles = useStyles();
|
||||
|
||||
const filterQueryWithTimeRange = useMemo(() => {
|
||||
return addTimerangeAndDefaultFilterToQuery(
|
||||
globalFilter.filterQuery,
|
||||
globalFilter.startDate,
|
||||
globalFilter.endDate
|
||||
);
|
||||
}, [globalFilter.filterQuery, globalFilter.startDate, globalFilter.endDate]);
|
||||
|
||||
const { data, isLoading } = useFetchPercentWidgetData(
|
||||
onReduce,
|
||||
filterQueryWithTimeRange,
|
||||
widgetKey,
|
||||
groupedBy,
|
||||
countBy,
|
||||
indexPattern?.title
|
||||
);
|
||||
|
||||
const { getFilterForValueButton, getFilterOutValueButton, filterManager } = useSetFilter();
|
||||
const dataValueSum = useMemo(
|
||||
() => (data ? Object.keys(data).reduce((sumSoFar, current) => sumSoFar + data[current], 0) : 0),
|
||||
[data]
|
||||
);
|
||||
const filterButtons = useMemo(() => {
|
||||
const result: FilterButtons = {
|
||||
filterForButtons: [],
|
||||
filterOutButtons: [],
|
||||
};
|
||||
Object.keys(dataValueMap).forEach((groupedByValue) => {
|
||||
if (!dataValueMap[groupedByValue].shouldHideFilter) {
|
||||
result.filterForButtons.push(
|
||||
getFilterForValueButton({
|
||||
field: dataValueMap[groupedByValue].fieldName,
|
||||
filterManager,
|
||||
size: 'xs',
|
||||
onClick: () => {},
|
||||
onFilterAdded: () => {},
|
||||
ownFocus: false,
|
||||
showTooltip: true,
|
||||
value: [groupedByValue],
|
||||
dataViewId,
|
||||
})
|
||||
);
|
||||
result.filterOutButtons.push(
|
||||
getFilterOutValueButton({
|
||||
field: dataValueMap[groupedByValue].fieldName,
|
||||
filterManager,
|
||||
size: 'xs',
|
||||
onClick: () => {},
|
||||
onFilterAdded: () => {},
|
||||
ownFocus: false,
|
||||
showTooltip: true,
|
||||
value: [groupedByValue],
|
||||
dataViewId,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [dataValueMap, dataViewId, filterManager, getFilterForValueButton, getFilterOutValueButton]);
|
||||
|
||||
return (
|
||||
<div css={styles.container}>
|
||||
{isLoading && (
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
color="accent"
|
||||
position="absolute"
|
||||
data-test-subj={LOADING_TEST_ID}
|
||||
/>
|
||||
)}
|
||||
<div css={styles.title}>{title}</div>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{Object.keys(dataValueMap).map((groupedByValue, idx) => {
|
||||
const value = data?.[groupedByValue] || 0;
|
||||
return (
|
||||
<EuiFlexItem
|
||||
key={`percentage-widget--${dataValueMap[groupedByValue].name}`}
|
||||
onMouseEnter={() => setHoveredFilter(idx)}
|
||||
onMouseLeave={() => setHoveredFilter(null)}
|
||||
data-test-subj={PERCENT_DATA_TEST_ID}
|
||||
>
|
||||
<EuiText size="xs" css={styles.dataInfo}>
|
||||
{dataValueMap[groupedByValue].name}
|
||||
{hoveredFilter === idx && (
|
||||
<div css={styles.filters}>
|
||||
{filterButtons.filterForButtons[idx]}
|
||||
{filterButtons.filterOutButtons[idx]}
|
||||
</div>
|
||||
)}
|
||||
<span css={styles.dataValue}>{value}</span>
|
||||
</EuiText>
|
||||
<div css={styles.percentageBackground}>
|
||||
<div
|
||||
css={{
|
||||
...styles.percentageBar,
|
||||
width: `${(value / dataValueSum || 0) * 100}%`,
|
||||
backgroundColor: dataValueMap[groupedByValue].color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import type { CSSObject } from '@emotion/react';
|
||||
import { useEuiTheme } from '../../hooks';
|
||||
|
||||
export const useStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const cached = useMemo(() => {
|
||||
const { size, colors, font, border } = euiTheme;
|
||||
|
||||
const container: CSSObject = {
|
||||
padding: size.base,
|
||||
border: border.thin,
|
||||
borderRadius: border.radius.medium,
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
};
|
||||
|
||||
const title: CSSObject = {
|
||||
marginBottom: size.m,
|
||||
};
|
||||
|
||||
const dataInfo: CSSObject = {
|
||||
marginBottom: size.xs,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '18px',
|
||||
};
|
||||
|
||||
const dataValue: CSSObject = {
|
||||
fontWeight: font.weight.semiBold,
|
||||
marginLeft: 'auto',
|
||||
};
|
||||
|
||||
const filters: CSSObject = {
|
||||
marginLeft: size.s,
|
||||
};
|
||||
|
||||
const percentageBackground: CSSObject = {
|
||||
position: 'relative',
|
||||
backgroundColor: colors.lightShade,
|
||||
height: size.xs,
|
||||
borderRadius: border.radius.small,
|
||||
};
|
||||
|
||||
const percentageBar: CSSObject = {
|
||||
position: 'absolute',
|
||||
height: size.xs,
|
||||
borderRadius: border.radius.small,
|
||||
};
|
||||
|
||||
const loadingSpinner: CSSObject = {
|
||||
alignItems: 'center',
|
||||
margin: `${size.xs} auto ${size.xl} auto`,
|
||||
};
|
||||
|
||||
return {
|
||||
container,
|
||||
title,
|
||||
dataInfo,
|
||||
dataValue,
|
||||
filters,
|
||||
percentageBackground,
|
||||
percentageBar,
|
||||
loadingSpinner,
|
||||
};
|
||||
}, [euiTheme]);
|
||||
|
||||
return cached;
|
||||
};
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 126 KiB |
|
@ -1,461 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Tree view Breadcrumb component When Breadcrumb is mounted renders Breadcrumb button content correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-responsive-l-spaceBetween-stretch-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-grow-1"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Cluster breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-clusterId"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="cluster"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Namespace breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-namespace"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="namespace"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Pod breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-pod"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="kubernetesPod"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Container Image breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-containerImage"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="container"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
class="euiButtonEmpty emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__content emotion-euiButtonDisplayContent"
|
||||
>
|
||||
<span
|
||||
class="eui-textTruncate euiButtonEmpty__text"
|
||||
>
|
||||
selected image
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tree view Breadcrumb component When Breadcrumb is mounted should display cluster icon button when no cluster name is provided 1`] = `
|
||||
<div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-responsive-l-spaceBetween-stretch-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-grow-1"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Cluster breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-clusterId"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="cluster"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Namespace breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-namespace"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="namespace"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Pod breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-pod"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="kubernetesPod"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Container Image breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-containerImage"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="container"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
class="euiButtonEmpty emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__content emotion-euiButtonDisplayContent"
|
||||
>
|
||||
<span
|
||||
class="eui-textTruncate euiButtonEmpty__text"
|
||||
>
|
||||
selected image
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tree view Breadcrumb component When Breadcrumb is mounted should render breadcrumb icons 1`] = `
|
||||
<div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-responsive-l-spaceBetween-stretch-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-grow-1"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Cluster breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-clusterId"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="cluster"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Namespace breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-namespace"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="namespace"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Node breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-node"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="kubernetesNode"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Pod breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-pod"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="kubernetesPod"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Container Image breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-containerImage"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="container"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
class="euiButtonEmpty emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__content emotion-euiButtonDisplayContent"
|
||||
>
|
||||
<span
|
||||
class="eui-textTruncate euiButtonEmpty__text"
|
||||
>
|
||||
selected image
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tree view Breadcrumb component When Breadcrumb is mounted should render last breadcrumb content only 1`] = `
|
||||
<div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-responsive-l-spaceBetween-stretch-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-grow-1"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Cluster breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-clusterId"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="cluster"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Node breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-node"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="kubernetesNode"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
aria-label="Click Container Image breadcrumb"
|
||||
class="euiButtonIcon emotion-EuiButtonIcon"
|
||||
data-test-subj="kubernetesSecurityBreadcrumbIcon-containerImage"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="container"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<button
|
||||
class="euiButtonEmpty emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__content emotion-euiButtonDisplayContent"
|
||||
>
|
||||
<span
|
||||
class="eui-textTruncate euiButtonEmpty__text"
|
||||
>
|
||||
selected image
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KubernetesCollectionMap, KubernetesTreeViewLevels } from '../../../types';
|
||||
import { showBreadcrumbDisplayText } from './helper';
|
||||
|
||||
describe('showBreadcrumbDisplayText()', () => {
|
||||
it('should return true when last breadcrumb is clusterId or clusterName', () => {
|
||||
const MOCK_TREE_SELECTION: Partial<KubernetesCollectionMap> = {
|
||||
clusterId: 'selected cluster id',
|
||||
clusterName: 'selected cluster name',
|
||||
};
|
||||
|
||||
expect(showBreadcrumbDisplayText(MOCK_TREE_SELECTION, KubernetesTreeViewLevels.clusterId)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when collectionType is not the last breadcrumb ', () => {
|
||||
const MOCK_TREE_SELECTION: Partial<KubernetesCollectionMap> = {
|
||||
clusterId: 'selected cluster id',
|
||||
clusterName: 'selected cluster name',
|
||||
node: 'selected node name',
|
||||
};
|
||||
|
||||
expect(showBreadcrumbDisplayText(MOCK_TREE_SELECTION, KubernetesTreeViewLevels.clusterId)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should true when node is the last breadcrumb ', () => {
|
||||
const MOCK_TREE_SELECTION: Partial<KubernetesCollectionMap> = {
|
||||
clusterId: 'selected cluster id',
|
||||
clusterName: 'selected cluster name',
|
||||
node: 'selected node name',
|
||||
};
|
||||
|
||||
expect(showBreadcrumbDisplayText(MOCK_TREE_SELECTION, KubernetesTreeViewLevels.node)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should true when pod is the last breadcrumb ', () => {
|
||||
const MOCK_TREE_SELECTION: Partial<KubernetesCollectionMap> = {
|
||||
clusterId: 'selected cluster id',
|
||||
clusterName: 'selected cluster name',
|
||||
node: 'selected node name',
|
||||
pod: 'selected pod name',
|
||||
};
|
||||
|
||||
expect(showBreadcrumbDisplayText(MOCK_TREE_SELECTION, KubernetesTreeViewLevels.pod)).toBe(true);
|
||||
});
|
||||
it('should true when container image is the last breadcrumb ', () => {
|
||||
const MOCK_TREE_SELECTION: Partial<KubernetesCollectionMap> = {
|
||||
clusterId: 'selected cluster id',
|
||||
clusterName: 'selected cluster name',
|
||||
node: 'selected node name',
|
||||
pod: 'selected pod name',
|
||||
containerImage: 'selected container image',
|
||||
};
|
||||
|
||||
expect(
|
||||
showBreadcrumbDisplayText(MOCK_TREE_SELECTION, KubernetesTreeViewLevels.containerImage)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KubernetesCollectionMap, KubernetesTreeViewLevels } from '../../../types';
|
||||
|
||||
export const showBreadcrumbDisplayText = (
|
||||
treeNavSelection: Partial<KubernetesCollectionMap<string>>,
|
||||
collectionType: string
|
||||
): boolean => {
|
||||
const resourceNames = Object.keys(treeNavSelection);
|
||||
const lastSelectedResourceName = resourceNames[resourceNames.length - 1];
|
||||
|
||||
const isClusterNameSelected =
|
||||
lastSelectedResourceName === KubernetesTreeViewLevels.clusterName &&
|
||||
collectionType === KubernetesTreeViewLevels.clusterId;
|
||||
|
||||
const isLastSelectedCollectionType = collectionType === lastSelectedResourceName;
|
||||
return isClusterNameSelected || isLastSelectedCollectionType;
|
||||
};
|
|
@ -1,138 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../../test';
|
||||
import { KubernetesCollectionMap } from '../../../types';
|
||||
import { Breadcrumb } from '.';
|
||||
|
||||
const MOCK_TREE_SELECTION: KubernetesCollectionMap = {
|
||||
clusterId: 'selected cluster id',
|
||||
clusterName: 'selected cluster name',
|
||||
namespace: 'selected namespace',
|
||||
node: 'selected node',
|
||||
pod: 'selected pod',
|
||||
containerImage: 'selected image',
|
||||
};
|
||||
|
||||
describe('Tree view Breadcrumb component', () => {
|
||||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
let onSelect: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
onSelect = jest.fn();
|
||||
});
|
||||
|
||||
describe('When Breadcrumb is mounted', () => {
|
||||
it('renders Breadcrumb button content correctly', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<Breadcrumb
|
||||
treeNavSelection={{ ...MOCK_TREE_SELECTION, node: undefined }}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.clusterName!)).toBeNull();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.namespace!)).toBeNull();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.node!)).toBeFalsy();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.pod!)).toBeNull();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.containerImage!)).toBeVisible();
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render breadcrumb icons', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<Breadcrumb treeNavSelection={MOCK_TREE_SELECTION} onSelect={onSelect} />
|
||||
);
|
||||
|
||||
expect(
|
||||
renderResult.queryByTestId('kubernetesSecurityBreadcrumbIcon-clusterId')
|
||||
).toBeVisible();
|
||||
expect(renderResult.queryByTestId('kubernetesSecurityBreadcrumbIcon-node')).toBeVisible();
|
||||
expect(renderResult.queryByTestId('kubernetesSecurityBreadcrumbIcon-pod')).toBeVisible();
|
||||
expect(
|
||||
renderResult.queryByTestId('kubernetesSecurityBreadcrumbIcon-containerImage')
|
||||
).toBeVisible();
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
it('returns null when no selected collection', async () => {
|
||||
renderResult = mockedContext.render(<Breadcrumb treeNavSelection={{}} onSelect={onSelect} />);
|
||||
expect(renderResult.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should display cluster icon button when no cluster name is provided', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<Breadcrumb
|
||||
treeNavSelection={{
|
||||
...MOCK_TREE_SELECTION,
|
||||
clusterName: undefined,
|
||||
node: undefined,
|
||||
}}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
renderResult.queryByTestId('kubernetesSecurityBreadcrumbIcon-clusterId')
|
||||
).toBeVisible();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.clusterId!)).toBeNull();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.containerImage!)).toBeVisible();
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should return null when no cluster in selection', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<Breadcrumb
|
||||
treeNavSelection={{ ...MOCK_TREE_SELECTION, clusterId: undefined }}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(renderResult.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('clicking on breadcrumb item triggers onSelect', async () => {
|
||||
const mockPodNavSelection = {
|
||||
clusterId: 'selected cluster id',
|
||||
clusterName: 'selected cluster name',
|
||||
namespace: 'selected namespace',
|
||||
node: 'selected node',
|
||||
pod: 'selected pod',
|
||||
};
|
||||
renderResult = mockedContext.render(
|
||||
<Breadcrumb treeNavSelection={mockPodNavSelection} onSelect={onSelect} />
|
||||
);
|
||||
expect(renderResult.queryByText(mockPodNavSelection.pod)).toBeVisible();
|
||||
renderResult.getByText(mockPodNavSelection.pod).click();
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render last breadcrumb content only', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<Breadcrumb
|
||||
treeNavSelection={{
|
||||
clusterId: MOCK_TREE_SELECTION.clusterId,
|
||||
clusterName: MOCK_TREE_SELECTION.clusterName,
|
||||
node: MOCK_TREE_SELECTION.node,
|
||||
containerImage: MOCK_TREE_SELECTION.containerImage,
|
||||
}}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.clusterName!)).toBeNull();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.namespace!)).toBeNull();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.node!)).toBeNull();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.pod!)).toBeNull();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION.containerImage!)).toBeVisible();
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,154 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { useEuiTheme } from '../../../hooks';
|
||||
import {
|
||||
KubernetesCollectionMap,
|
||||
KubernetesCollection,
|
||||
TreeViewIconProps,
|
||||
KubernetesTreeViewLevels,
|
||||
} from '../../../types';
|
||||
import { useStyles } from './styles';
|
||||
import { KUBERNETES_COLLECTION_ICONS_PROPS } from '../helpers';
|
||||
import { showBreadcrumbDisplayText } from './helper';
|
||||
import { BREADCRUMBS_CLUSTER_TREE_VIEW_LEVELS } from '../translations';
|
||||
interface BreadcrumbDeps {
|
||||
treeNavSelection: Partial<KubernetesCollectionMap>;
|
||||
onSelect: (selection: Partial<KubernetesCollectionMap>) => void;
|
||||
}
|
||||
|
||||
export const Breadcrumb = ({ treeNavSelection, onSelect }: BreadcrumbDeps) => {
|
||||
const styles = useStyles();
|
||||
const { euiVars } = useEuiTheme();
|
||||
const onBreadCrumbClick = useCallback(
|
||||
(collectionType: string) => {
|
||||
return async () => {
|
||||
const selectionCopy = { ...treeNavSelection };
|
||||
switch (collectionType) {
|
||||
case KubernetesTreeViewLevels.clusterId: {
|
||||
onSelect({
|
||||
clusterId: treeNavSelection.clusterId,
|
||||
clusterName: treeNavSelection.clusterName,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case KubernetesTreeViewLevels.namespace:
|
||||
case KubernetesTreeViewLevels.node: {
|
||||
delete selectionCopy.pod;
|
||||
delete selectionCopy.containerImage;
|
||||
onSelect(selectionCopy);
|
||||
break;
|
||||
}
|
||||
case KubernetesTreeViewLevels.pod: {
|
||||
delete selectionCopy.containerImage;
|
||||
onSelect(selectionCopy);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[onSelect, treeNavSelection]
|
||||
);
|
||||
|
||||
const renderBreadcrumbLink = useCallback(
|
||||
(
|
||||
collectionType: KubernetesCollection,
|
||||
treeViewIconProps: TreeViewIconProps,
|
||||
isBolded: boolean,
|
||||
hasRightArrow: boolean = true
|
||||
) => {
|
||||
const clusterLevel = BREADCRUMBS_CLUSTER_TREE_VIEW_LEVELS[collectionType];
|
||||
const resourceName =
|
||||
collectionType === KubernetesTreeViewLevels.clusterId
|
||||
? treeNavSelection.clusterName || treeNavSelection.clusterId
|
||||
: treeNavSelection[collectionType];
|
||||
|
||||
const tooltip = `${clusterLevel}: ${resourceName}`;
|
||||
const showBreadcrumbText = showBreadcrumbDisplayText(treeNavSelection, collectionType);
|
||||
const { type: iconType, euiVarColor } = treeViewIconProps;
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasRightArrow && <EuiIcon css={styles.breadcrumbRightIcon} type="arrowRight" size="s" />}
|
||||
<EuiToolTip content={tooltip}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`kubernetesSecurityBreadcrumbIcon-${collectionType}`}
|
||||
iconType={iconType}
|
||||
css={styles.breadcrumbIconColor(euiVars[euiVarColor])}
|
||||
aria-label={`Click ${clusterLevel} breadcrumb`}
|
||||
onClick={onBreadCrumbClick(collectionType)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
{showBreadcrumbText && (
|
||||
<EuiToolTip content={tooltip}>
|
||||
<EuiButtonEmpty
|
||||
css={isBolded ? styles.breadcrumbButtonBold : styles.breadcrumbButton}
|
||||
color="text"
|
||||
onClick={onBreadCrumbClick(collectionType)}
|
||||
>
|
||||
{resourceName}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
[onBreadCrumbClick, styles, treeNavSelection, euiVars]
|
||||
);
|
||||
|
||||
if (!treeNavSelection.clusterId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div css={styles.breadcrumb}>
|
||||
<EuiFlexGroup justifyContent={'spaceBetween'}>
|
||||
<EuiFlexItem css={styles.breadcrumbsWrapper}>
|
||||
{renderBreadcrumbLink(
|
||||
KubernetesTreeViewLevels.clusterId,
|
||||
KUBERNETES_COLLECTION_ICONS_PROPS.clusterId,
|
||||
!(treeNavSelection.namespace || treeNavSelection.node),
|
||||
false
|
||||
)}
|
||||
{treeNavSelection.namespace &&
|
||||
renderBreadcrumbLink(
|
||||
KubernetesTreeViewLevels.namespace,
|
||||
KUBERNETES_COLLECTION_ICONS_PROPS.namespace,
|
||||
!treeNavSelection.pod
|
||||
)}
|
||||
{treeNavSelection.node &&
|
||||
renderBreadcrumbLink(
|
||||
KubernetesTreeViewLevels.node,
|
||||
KUBERNETES_COLLECTION_ICONS_PROPS.node,
|
||||
!treeNavSelection.pod
|
||||
)}
|
||||
{treeNavSelection.pod &&
|
||||
renderBreadcrumbLink(
|
||||
KubernetesTreeViewLevels.pod,
|
||||
KUBERNETES_COLLECTION_ICONS_PROPS.pod,
|
||||
!treeNavSelection.containerImage
|
||||
)}
|
||||
{treeNavSelection.containerImage &&
|
||||
renderBreadcrumbLink(
|
||||
KubernetesTreeViewLevels.containerImage,
|
||||
KUBERNETES_COLLECTION_ICONS_PROPS.containerImage,
|
||||
true
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { CSSObject } from '@emotion/react';
|
||||
import { useEuiTheme } from '../../../hooks';
|
||||
|
||||
export const useStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const cached = useMemo(() => {
|
||||
const { colors, size, font, border } = euiTheme;
|
||||
|
||||
const breadcrumb: CSSObject = {
|
||||
borderBottom: border.thin,
|
||||
borderColor: colors.lightShade,
|
||||
paddingBottom: size.s,
|
||||
marginBottom: size.m,
|
||||
};
|
||||
|
||||
const breadcrumbButton: CSSObject = {
|
||||
height: 'fit-content',
|
||||
maxWidth: '248px',
|
||||
fontSize: size.m,
|
||||
fontWeight: font.weight.regular,
|
||||
'.euiButtonEmpty': {
|
||||
paddingInline: size.xs,
|
||||
},
|
||||
};
|
||||
|
||||
const breadcrumbButtonBold: CSSObject = {
|
||||
...breadcrumbButton,
|
||||
fontWeight: font.weight.semiBold,
|
||||
};
|
||||
|
||||
const breadcrumbRightIcon: CSSObject = {
|
||||
marginRight: size.xs,
|
||||
};
|
||||
|
||||
const breadcrumbsWrapper: CSSObject = { flexDirection: 'row', alignItems: 'center' };
|
||||
|
||||
const breadcrumbIconColor = (color: string): CSSObject => ({
|
||||
color,
|
||||
});
|
||||
|
||||
return {
|
||||
breadcrumb,
|
||||
breadcrumbButton,
|
||||
breadcrumbButtonBold,
|
||||
breadcrumbRightIcon,
|
||||
breadcrumbsWrapper,
|
||||
breadcrumbIconColor,
|
||||
};
|
||||
}, [euiTheme]);
|
||||
|
||||
return cached;
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
import { useTreeView, UseTreeViewProps } from './hooks';
|
||||
|
||||
type TreeViewContextType = ReturnType<typeof useTreeView>;
|
||||
|
||||
const TreeViewContext = createContext<TreeViewContextType | null>(null);
|
||||
|
||||
export const useTreeViewContext = () => {
|
||||
const context = useContext(TreeViewContext);
|
||||
if (!context) {
|
||||
throw new Error('useTreeViewContext must be called within an TreeViewContextProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
type TreeViewContextProviderProps = {
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export const TreeViewContextProvider = ({
|
||||
children,
|
||||
...useTreeViewProps
|
||||
}: TreeViewContextProviderProps & UseTreeViewProps) => {
|
||||
return (
|
||||
<TreeViewContext.Provider value={useTreeView(useTreeViewProps)}>
|
||||
{children}
|
||||
</TreeViewContext.Provider>
|
||||
);
|
||||
};
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { FC } from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { focusNextElement } from './helpers';
|
||||
|
||||
// dummy component for testing
|
||||
const TestButtonList: FC<any> = (props: any) => (
|
||||
<>
|
||||
{[...Array(100)].map((_, idx) => (
|
||||
<button key={idx} {...props}>
|
||||
Button {idx}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
describe('DynamicTreeView component', () => {
|
||||
it('Should focus the next element', async () => {
|
||||
const onKeyDown = (e: any) => {
|
||||
focusNextElement(e, 'button', 'next');
|
||||
};
|
||||
const wrapper = render(<TestButtonList onKeyDown={onKeyDown} />);
|
||||
wrapper.getByText('Button 40').focus();
|
||||
|
||||
await userEvent.keyboard('{ArrowRight}');
|
||||
expect(wrapper.getByText('Button 41')).toHaveFocus();
|
||||
await userEvent.keyboard('{ArrowRight}');
|
||||
expect(wrapper.getByText('Button 42')).toHaveFocus();
|
||||
});
|
||||
it('Should focus the previous element', async () => {
|
||||
const onKeyDown = (e: any) => {
|
||||
focusNextElement(e, 'button', 'prev');
|
||||
};
|
||||
const wrapper = render(<TestButtonList onKeyDown={onKeyDown} />);
|
||||
wrapper.getByText('Button 40').focus();
|
||||
|
||||
await userEvent.keyboard('{ArrowLeft}');
|
||||
expect(wrapper.getByText('Button 39')).toHaveFocus();
|
||||
await userEvent.keyboard('{ArrowLeft}');
|
||||
expect(wrapper.getByText('Button 38')).toHaveFocus();
|
||||
});
|
||||
});
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KeyboardEvent, MouseEvent } from 'react';
|
||||
|
||||
export const disableEventDefaults = (event: KeyboardEvent | MouseEvent<SVGElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
export const focusNextElement = (
|
||||
event: KeyboardEvent,
|
||||
selector: string,
|
||||
direction: 'prev' | 'next'
|
||||
) => {
|
||||
const list = Array.from(document.querySelectorAll(selector));
|
||||
const currentIndex = list.indexOf(event.currentTarget);
|
||||
if (currentIndex > -1) {
|
||||
const nextButton = list[currentIndex + (direction === 'next' ? +1 : -1)] as HTMLElement;
|
||||
if (nextButton) {
|
||||
disableEventDefaults(event);
|
||||
nextButton.focus();
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { QueryDslQueryContainerBool } from '../../../types';
|
||||
import {
|
||||
QUERY_KEY_PROCESS_EVENTS,
|
||||
AGGREGATE_ROUTE,
|
||||
MULTI_TERMS_AGGREGATE_ROUTE,
|
||||
ORCHESTRATOR_CLUSTER_NAME,
|
||||
CURRENT_API_VERSION,
|
||||
} from '../../../../common/constants';
|
||||
import { AggregateBucketPaginationResult, MultiTermsBucket } from '../../../../common/types';
|
||||
import { KUBERNETES_COLLECTION_FIELDS } from '../helpers';
|
||||
|
||||
export const useFetchDynamicTreeView = (
|
||||
query: QueryDslQueryContainerBool,
|
||||
groupBy: string,
|
||||
index?: string,
|
||||
enabled?: boolean
|
||||
) => {
|
||||
const { http } = useKibana<CoreStart>().services;
|
||||
const cachingKeys = [QUERY_KEY_PROCESS_EVENTS, query, groupBy, index];
|
||||
|
||||
return useInfiniteQuery<AggregateBucketPaginationResult>(
|
||||
cachingKeys,
|
||||
async ({ pageParam = 0 }) => {
|
||||
if (groupBy === KUBERNETES_COLLECTION_FIELDS.clusterId) {
|
||||
const { buckets } = await http.get<any>(MULTI_TERMS_AGGREGATE_ROUTE, {
|
||||
version: '1',
|
||||
query: {
|
||||
query: JSON.stringify(query),
|
||||
groupBys: JSON.stringify([
|
||||
{
|
||||
field: groupBy,
|
||||
},
|
||||
{
|
||||
field: ORCHESTRATOR_CLUSTER_NAME,
|
||||
missing: '',
|
||||
},
|
||||
]),
|
||||
page: pageParam,
|
||||
perPage: 50,
|
||||
index,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
buckets: buckets.map((bucket: MultiTermsBucket) => ({
|
||||
...bucket,
|
||||
key_as_string: bucket.key[1],
|
||||
key: bucket.key[0],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return await http.get<any>(AGGREGATE_ROUTE, {
|
||||
version: CURRENT_API_VERSION,
|
||||
query: {
|
||||
query: JSON.stringify(query),
|
||||
groupBy,
|
||||
page: pageParam,
|
||||
perPage: 50,
|
||||
index,
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled,
|
||||
getNextPageParam: (lastPage, pages) => (lastPage.hasNextPage ? pages.length : undefined),
|
||||
}
|
||||
);
|
||||
};
|
|
@ -1,203 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../../test';
|
||||
import { DynamicTreeView } from '.';
|
||||
import { clusterResponseMock, nodeResponseMock } from '../mocks';
|
||||
import { TreeViewContextProvider } from '../contexts';
|
||||
|
||||
describe('DynamicTreeView component', () => {
|
||||
let render: (props?: any) => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
let mockedApi: AppContextTestRender['coreStart']['http']['get'];
|
||||
|
||||
const defaultProps = {
|
||||
globalFilter: {
|
||||
startDate: Date.now().toString(),
|
||||
endDate: (Date.now() + 1).toString(),
|
||||
},
|
||||
indexPattern: {
|
||||
title: '*-logs',
|
||||
},
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
mockedApi = mockedContext.coreStart.http.get;
|
||||
mockedApi.mockResolvedValue(clusterResponseMock);
|
||||
render = (props) =>
|
||||
(renderResult = mockedContext.render(
|
||||
<TreeViewContextProvider {...defaultProps}>
|
||||
<DynamicTreeView
|
||||
query={{
|
||||
bool: {
|
||||
filter: [],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
}}
|
||||
tree={[
|
||||
{
|
||||
key: 'clusterId',
|
||||
name: 'clusterId',
|
||||
namePlural: 'clusters',
|
||||
type: 'clusterId',
|
||||
iconProps: {
|
||||
type: 'cluster',
|
||||
},
|
||||
},
|
||||
]}
|
||||
onSelect={(selectionDepth, key, type) => {}}
|
||||
{...props}
|
||||
/>
|
||||
</TreeViewContextProvider>
|
||||
));
|
||||
});
|
||||
|
||||
describe('When DynamicTreeView is mounted', () => {
|
||||
it('should show loading state while retrieving empty data and hide it when settled', async () => {
|
||||
render();
|
||||
expect(renderResult.queryByText(/loading/i)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(renderResult.queryByText(/loading/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DynamicTreeView parent level', () => {
|
||||
const key = 'orchestrator.cluster.id';
|
||||
const tree = [
|
||||
{
|
||||
key,
|
||||
name: 'cluster',
|
||||
namePlural: 'clusters',
|
||||
type: 'clusterId',
|
||||
iconProps: {
|
||||
type: 'cluster',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('should make a api call with group based on tree parameters', async () => {
|
||||
render({
|
||||
tree,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi).toHaveBeenCalledWith(
|
||||
'/internal/kubernetes_security/multi_terms_aggregate',
|
||||
{
|
||||
query: {
|
||||
groupBys: `[{"field":"${key}"},{"field":"orchestrator.cluster.name","missing":""}]`,
|
||||
index: '*-logs',
|
||||
page: 0,
|
||||
perPage: 50,
|
||||
query: '{"bool":{"filter":[],"must":[],"must_not":[],"should":[]}}',
|
||||
},
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the parent level based on api response', async () => {
|
||||
render({
|
||||
tree,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
['awp-demo-gke-main', 'awp-demo-gke-test'].forEach((cluster) => {
|
||||
expect(renderResult.queryByText(cluster)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should trigger a callback when tree node is clicked', async () => {
|
||||
const callback = jest.fn();
|
||||
render({ tree, onSelect: callback });
|
||||
|
||||
await waitFor(() => {
|
||||
renderResult.getByRole('button', { name: 'awp-demo-gke-main' }).click();
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DynamicTreeView children', () => {
|
||||
const tree = [
|
||||
{
|
||||
key: 'orchestrator.cluster.id',
|
||||
name: 'clusterId',
|
||||
namePlural: 'clusters',
|
||||
type: 'clusterId',
|
||||
iconProps: {
|
||||
type: 'cluster',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'node',
|
||||
name: 'node',
|
||||
namePlural: 'nodes',
|
||||
type: 'node',
|
||||
iconProps: {
|
||||
type: 'node',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const parent = 'awp-demo-gke-main';
|
||||
|
||||
it('should make a children api call with filter when parent is expanded', async () => {
|
||||
render({ tree });
|
||||
await waitFor(() => {
|
||||
renderResult.getByRole('button', { name: parent }).click();
|
||||
});
|
||||
|
||||
mockedApi.mockResolvedValueOnce(nodeResponseMock);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi).toHaveBeenCalledWith('/internal/kubernetes_security/aggregate', {
|
||||
query: {
|
||||
groupBy: 'node',
|
||||
index: '*-logs',
|
||||
page: 0,
|
||||
perPage: 50,
|
||||
query: `{"bool":{"filter":[{"term":{"orchestrator.cluster.id":"${parent}"}}],"must":[],"must_not":[],"should":[]}}`,
|
||||
},
|
||||
version: '1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render children when parent is expanded based on api request', async () => {
|
||||
render({ tree });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.getByRole('button', { name: parent })).toBeTruthy();
|
||||
mockedApi.mockResolvedValueOnce(nodeResponseMock);
|
||||
renderResult.getByRole('button', { name: parent }).click();
|
||||
});
|
||||
|
||||
// check if children has loading state
|
||||
await waitFor(() => {
|
||||
expect(renderResult.queryByText(/loading/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
['default', 'kube-system', 'production', 'qa', 'staging'].forEach((node) => {
|
||||
expect(renderResult.queryByText(node)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,344 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef, KeyboardEvent, useMemo } from 'react';
|
||||
import {
|
||||
EuiTreeView,
|
||||
EuiText,
|
||||
EuiI18n,
|
||||
EuiScreenReaderOnly,
|
||||
EuiBadge,
|
||||
keys,
|
||||
EuiLoadingSpinner,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
// @ts-expect-error style types not defined, but they exist
|
||||
import { euiTreeViewStyles } from '@elastic/eui/lib/components/tree_view/tree_view.styles';
|
||||
|
||||
import {
|
||||
TREE_NAVIGATION_LOADING,
|
||||
TREE_NAVIGATION_EMPTY,
|
||||
TREE_NAVIGATION_SHOW_MORE,
|
||||
} from '../../../../common/translations';
|
||||
import { useFetchDynamicTreeView } from './hooks';
|
||||
import { useStyles } from './styles';
|
||||
import { disableEventDefaults, focusNextElement } from './helpers';
|
||||
import { useTreeViewContext } from '../contexts';
|
||||
import { TreeViewIcon } from '../tree_view_icon';
|
||||
import type { DynamicTreeViewProps, DynamicTreeViewItemProps } from './types';
|
||||
import { BREADCRUMBS_CLUSTER_TREE_VIEW_LEVELS } from '../translations';
|
||||
|
||||
const BUTTON_TEST_ID = 'kubernetesSecurity:dynamicTreeViewButton';
|
||||
|
||||
const focusNextButton = (event: KeyboardEvent) => {
|
||||
focusNextElement(event, `[data-test-subj="${BUTTON_TEST_ID}"]`, 'next');
|
||||
};
|
||||
const focusPreviousButton = (event: KeyboardEvent) => {
|
||||
focusNextElement(event, `[data-test-subj="${BUTTON_TEST_ID}"]`, 'prev');
|
||||
};
|
||||
|
||||
const DynamicTreeViewExpander = ({
|
||||
defaultExpanded = false,
|
||||
children,
|
||||
}: {
|
||||
defaultExpanded: boolean;
|
||||
children: (childrenProps: { isExpanded: boolean; onToggleExpand: () => void }) => JSX.Element;
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
|
||||
const onToggleExpand = () => {
|
||||
setIsExpanded((e) => !e);
|
||||
};
|
||||
|
||||
return children({
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
});
|
||||
};
|
||||
|
||||
export const DynamicTreeView = ({
|
||||
tree,
|
||||
depth = 0,
|
||||
selectionDepth = {},
|
||||
query,
|
||||
onSelect,
|
||||
selected = '',
|
||||
expanded = true,
|
||||
onKeyDown,
|
||||
}: DynamicTreeViewProps) => {
|
||||
const styles = useStyles(depth);
|
||||
const euiStyles = euiTreeViewStyles(useEuiTheme());
|
||||
const euiTreeViewCss = [euiStyles.euiTreeView, euiStyles.default];
|
||||
|
||||
const { indexPattern, setNoResults, setTreeNavSelection } = useTreeViewContext();
|
||||
|
||||
const { data, fetchNextPage, isFetchingNextPage, hasNextPage, isLoading } =
|
||||
useFetchDynamicTreeView(query, tree[depth].key, indexPattern, expanded);
|
||||
|
||||
const onLoadMoreKeydown = (event: React.KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case keys.ARROW_DOWN: {
|
||||
focusNextButton(event);
|
||||
break;
|
||||
}
|
||||
case keys.ARROW_UP: {
|
||||
focusPreviousButton(event);
|
||||
break;
|
||||
}
|
||||
case keys.ARROW_RIGHT: {
|
||||
disableEventDefaults(event);
|
||||
fetchNextPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (depth === 0 && data) {
|
||||
const noData = data.pages?.[0].buckets.length === 0;
|
||||
setNoResults(noData);
|
||||
|
||||
if (noData) {
|
||||
setTreeNavSelection({});
|
||||
}
|
||||
}
|
||||
}, [data, depth, setNoResults, setTreeNavSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [fetchNextPage, expanded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected && !depth && data && data.pages?.[0].buckets?.[0]?.key) {
|
||||
onSelect(
|
||||
{},
|
||||
tree[depth].type,
|
||||
data.pages[0].buckets[0].key,
|
||||
data.pages[0].buckets[0].key_as_string
|
||||
);
|
||||
}
|
||||
}, [data, depth, selected, onSelect, tree]);
|
||||
|
||||
const onClickNextPageHandler = () => {
|
||||
fetchNextPage();
|
||||
};
|
||||
|
||||
const itemList = useMemo(() => {
|
||||
return (
|
||||
data?.pages
|
||||
?.map((aggsData) => {
|
||||
return aggsData?.buckets;
|
||||
})
|
||||
.flat() || []
|
||||
);
|
||||
}, [data?.pages]);
|
||||
|
||||
return (
|
||||
<EuiText size="s" css={styles.euiTreeViewWrapper} hidden={!expanded} onKeyDown={onKeyDown}>
|
||||
{depth === 0 && (
|
||||
<EuiI18n
|
||||
token="euiTreeView.listNavigationInstructions"
|
||||
default="You can quickly navigate this list using arrow keys."
|
||||
>
|
||||
{(listNavigationInstructions: string) => (
|
||||
<EuiScreenReaderOnly>
|
||||
<p id="dynamicTreeViewInstructionId">{listNavigationInstructions}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
)}
|
||||
</EuiI18n>
|
||||
)}
|
||||
<ul
|
||||
css={euiTreeViewCss}
|
||||
aria-describedby={data?.pages?.length ? 'dynamicTreeViewInstructionId' : undefined}
|
||||
>
|
||||
{isLoading && (
|
||||
<EuiTreeView.Item
|
||||
id="dynamicTreeViewLoading"
|
||||
css={styles.nonInteractiveItem}
|
||||
icon={<EuiLoadingSpinner size="s" />}
|
||||
label={TREE_NAVIGATION_LOADING}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && !itemList.length && (
|
||||
<EuiTreeView.Item
|
||||
id="dynamicTreeViewEmpty"
|
||||
css={styles.nonInteractiveItem}
|
||||
label={TREE_NAVIGATION_EMPTY}
|
||||
/>
|
||||
)}
|
||||
{itemList.map((aggData) => {
|
||||
const queryFilter = {
|
||||
...query,
|
||||
bool: {
|
||||
...query.bool,
|
||||
filter: [...query.bool.filter, { term: { [tree[depth].key]: aggData.key } }],
|
||||
},
|
||||
};
|
||||
|
||||
const defaultExpanded = selected.indexOf('' + aggData.key) > 0;
|
||||
|
||||
return (
|
||||
<DynamicTreeViewExpander key={aggData.key} defaultExpanded={defaultExpanded}>
|
||||
{({ isExpanded, onToggleExpand }) => (
|
||||
<DynamicTreeViewItem
|
||||
aggData={aggData}
|
||||
depth={depth}
|
||||
expanded={expanded}
|
||||
isExpanded={isExpanded}
|
||||
onSelect={onSelect}
|
||||
onToggleExpand={onToggleExpand}
|
||||
query={queryFilter}
|
||||
selected={selected}
|
||||
selectionDepth={selectionDepth}
|
||||
tree={tree}
|
||||
/>
|
||||
)}
|
||||
</DynamicTreeViewExpander>
|
||||
);
|
||||
})}
|
||||
{hasNextPage && (
|
||||
<EuiTreeView.Item
|
||||
id="dynamicTreeViewLoadMore"
|
||||
css={styles.loadMoreButton}
|
||||
aria-label={TREE_NAVIGATION_SHOW_MORE(tree[depth].namePlural)}
|
||||
data-test-subj={BUTTON_TEST_ID}
|
||||
onKeyDown={(event: React.KeyboardEvent) => onLoadMoreKeydown(event)}
|
||||
onClick={onClickNextPageHandler}
|
||||
label={
|
||||
<EuiBadge
|
||||
css={styles.loadMoreBadge}
|
||||
iconSide="right"
|
||||
iconType={isFetchingNextPage ? EuiLoadingSpinner : 'arrowDown'}
|
||||
>
|
||||
{isFetchingNextPage
|
||||
? TREE_NAVIGATION_LOADING
|
||||
: TREE_NAVIGATION_SHOW_MORE(tree[depth].namePlural)}
|
||||
</EuiBadge>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
</EuiText>
|
||||
);
|
||||
};
|
||||
|
||||
const DynamicTreeViewItem = ({
|
||||
depth,
|
||||
tree,
|
||||
onToggleExpand,
|
||||
onSelect,
|
||||
aggData,
|
||||
selectionDepth,
|
||||
isExpanded,
|
||||
selected,
|
||||
expanded,
|
||||
query,
|
||||
}: DynamicTreeViewItemProps) => {
|
||||
const isLastNode = depth === tree.length - 1;
|
||||
const buttonRef = useRef<Record<string, any>>({});
|
||||
|
||||
const handleSelect = () => {
|
||||
if (tree[depth].type === 'clusterId') {
|
||||
onSelect(selectionDepth, tree[depth].type, aggData.key, aggData.key_as_string);
|
||||
} else {
|
||||
onSelect(selectionDepth, tree[depth].type, aggData.key);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyboardToggle = () => {
|
||||
if (!isLastNode) {
|
||||
onToggleExpand();
|
||||
}
|
||||
handleSelect();
|
||||
};
|
||||
|
||||
const onButtonToggle = () => {
|
||||
if (!isLastNode) {
|
||||
onToggleExpand();
|
||||
}
|
||||
handleSelect();
|
||||
};
|
||||
|
||||
// Enable keyboard navigation
|
||||
const onKeyDown = (event: React.KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case keys.ARROW_DOWN: {
|
||||
focusNextButton(event);
|
||||
break;
|
||||
}
|
||||
case keys.ARROW_UP: {
|
||||
focusPreviousButton(event);
|
||||
break;
|
||||
}
|
||||
case keys.ARROW_RIGHT: {
|
||||
if (!isExpanded && !isLastNode) {
|
||||
disableEventDefaults(event);
|
||||
onKeyboardToggle();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case keys.ARROW_LEFT: {
|
||||
if (isExpanded) {
|
||||
disableEventDefaults(event);
|
||||
onKeyboardToggle();
|
||||
}
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const onChildrenKeydown = (event: React.KeyboardEvent, key: string) => {
|
||||
if (event.key === keys.ARROW_LEFT) {
|
||||
disableEventDefaults(event);
|
||||
buttonRef.current[key].focus();
|
||||
}
|
||||
};
|
||||
|
||||
const clusterLevel = BREADCRUMBS_CLUSTER_TREE_VIEW_LEVELS[tree[depth].type];
|
||||
|
||||
return (
|
||||
<EuiTreeView.Item
|
||||
id={aggData.key_as_string || `${aggData.key}`}
|
||||
hasArrow={!isLastNode}
|
||||
isExpanded={isExpanded}
|
||||
onClick={onButtonToggle}
|
||||
onKeyDown={onKeyDown}
|
||||
icon={<TreeViewIcon {...tree[depth].iconProps} />}
|
||||
label={
|
||||
<EuiToolTip anchorClassName="eui-textTruncate" content={`${clusterLevel}: ${aggData.key}`}>
|
||||
<span>{aggData.key_as_string || aggData.key}</span>
|
||||
</EuiToolTip>
|
||||
}
|
||||
buttonRef={(el: HTMLButtonElement) => (buttonRef.current[aggData.key] = el)}
|
||||
data-test-subj={expanded ? BUTTON_TEST_ID : ''}
|
||||
>
|
||||
{!isLastNode && (
|
||||
<DynamicTreeView
|
||||
expanded={isExpanded}
|
||||
query={query}
|
||||
depth={depth + 1}
|
||||
selectionDepth={{
|
||||
...selectionDepth,
|
||||
[tree[depth].type]: aggData.key,
|
||||
...(tree[depth].type === 'clusterId' && {
|
||||
clusterName: aggData.key_as_string,
|
||||
}),
|
||||
}}
|
||||
tree={tree}
|
||||
onSelect={onSelect}
|
||||
selected={selected}
|
||||
onKeyDown={(event: React.KeyboardEvent) =>
|
||||
onChildrenKeydown(event, aggData.key.toString())
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</EuiTreeView.Item>
|
||||
);
|
||||
};
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { CSSObject } from '@emotion/react';
|
||||
import { useEuiTheme } from '../../../hooks';
|
||||
|
||||
export const useStyles = (depth: number) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const cached = useMemo(() => {
|
||||
const { size, colors, border } = euiTheme;
|
||||
|
||||
const loadMoreButton: CSSObject = {
|
||||
position: 'relative',
|
||||
textAlign: 'center',
|
||||
width: `calc(100% + ${depth * 24}px)`,
|
||||
marginLeft: `-${depth * 24}px`,
|
||||
'&::after': {
|
||||
content: `''`,
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
width: '100%',
|
||||
border: `${border.width.thin} dashed ${colors.mediumShade}`,
|
||||
left: 0,
|
||||
},
|
||||
'&:hover, &:focus': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
'.euiTreeView__nodeLabel': {
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
const loadMoreBadge: CSSObject = {
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
zIndex: 2,
|
||||
'.euiBadge__content': {
|
||||
gap: size.xs,
|
||||
},
|
||||
};
|
||||
const nonInteractiveItem: CSSObject = {
|
||||
pointerEvents: 'none',
|
||||
'&:hover, &:focus': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
};
|
||||
const euiTreeViewWrapper: CSSObject = {
|
||||
ul: {
|
||||
marginLeft: '0 !important',
|
||||
fontSize: 'inherit',
|
||||
},
|
||||
// Override default EUI max-height - `DynamicTreeView` has its own scrolling container
|
||||
'.euiTreeView__node': {
|
||||
maxBlockSize: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
loadMoreButton,
|
||||
loadMoreBadge,
|
||||
nonInteractiveItem,
|
||||
euiTreeViewWrapper,
|
||||
};
|
||||
}, [euiTheme, depth]);
|
||||
|
||||
return cached;
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KeyboardEventHandler } from 'react';
|
||||
import { QueryDslQueryContainerBool, KubernetesCollectionMap, DynamicTree } from '../../../types';
|
||||
|
||||
export type DynamicTreeViewProps = {
|
||||
tree: DynamicTree[];
|
||||
depth?: number;
|
||||
selectionDepth?: Partial<KubernetesCollectionMap>;
|
||||
query: QueryDslQueryContainerBool;
|
||||
onSelect: (
|
||||
selectionDepth: Partial<KubernetesCollectionMap>,
|
||||
type: string,
|
||||
key: string | number,
|
||||
clusterName?: string
|
||||
) => void;
|
||||
hasSelection?: boolean;
|
||||
selected?: string;
|
||||
expanded?: boolean;
|
||||
onKeyDown?: KeyboardEventHandler | undefined;
|
||||
};
|
||||
|
||||
export type DynamicTreeViewItemProps = Required<
|
||||
Omit<DynamicTreeViewProps, 'hasSelection' | 'onKeyDown'>
|
||||
> & {
|
||||
onToggleExpand: any;
|
||||
aggData: any;
|
||||
isExpanded: boolean;
|
||||
};
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiImage, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { CSSObject } from '@emotion/serialize';
|
||||
import icon from './assets/illustration_product_no_results_magnifying_glass.svg';
|
||||
|
||||
export const TREE_EMPTY_STATE = 'kubernetesSecurity:treeEmptyState';
|
||||
|
||||
const panelStyle: CSSObject = {
|
||||
maxWidth: 500,
|
||||
};
|
||||
|
||||
const wrapperStyle: CSSObject = {
|
||||
height: 262,
|
||||
};
|
||||
|
||||
export const EmptyState: React.FC = () => {
|
||||
return (
|
||||
<EuiPanel color="subdued" data-test-subj={TREE_EMPTY_STATE}>
|
||||
<EuiFlexGroup css={wrapperStyle} alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPanel hasBorder={true} css={panelStyle}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<EuiTitle>
|
||||
<h3 aria-level={2}>
|
||||
<FormattedMessage
|
||||
id="xpack.kubernetesSecurity.treeView.empty.title"
|
||||
defaultMessage="No results match your search criteria"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.kubernetesSecurity.treeView.empty.description"
|
||||
defaultMessage="Try searching over a longer period of time or modifying your search"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiImage size="200" alt="" url={icon} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
CLOUD_INSTANCE_NAME,
|
||||
CONTAINER_IMAGE_NAME,
|
||||
DEFAULT_FILTER_QUERY,
|
||||
ORCHESTRATOR_CLUSTER_ID,
|
||||
ORCHESTRATOR_CLUSTER_NAME,
|
||||
ORCHESTRATOR_NAMESPACE,
|
||||
ORCHESTRATOR_RESOURCE_ID,
|
||||
} from '../../../common/constants';
|
||||
import type {
|
||||
QueryDslQueryContainerBool,
|
||||
KubernetesCollectionMap,
|
||||
KubernetesCollection,
|
||||
TreeViewIconProps,
|
||||
} from '../../types';
|
||||
|
||||
export const KUBERNETES_COLLECTION_FIELDS: KubernetesCollectionMap = {
|
||||
clusterId: ORCHESTRATOR_CLUSTER_ID,
|
||||
clusterName: ORCHESTRATOR_CLUSTER_NAME,
|
||||
namespace: ORCHESTRATOR_NAMESPACE,
|
||||
node: CLOUD_INSTANCE_NAME,
|
||||
pod: ORCHESTRATOR_RESOURCE_ID,
|
||||
containerImage: CONTAINER_IMAGE_NAME,
|
||||
};
|
||||
|
||||
export const KUBERNETES_COLLECTION_ICONS_PROPS: KubernetesCollectionMap<TreeViewIconProps> = {
|
||||
clusterId: { type: 'cluster', euiVarColor: 'euiColorVis0' },
|
||||
clusterName: { type: 'cluster', euiVarColor: 'euiColorVis0' },
|
||||
namespace: { type: 'namespace', euiVarColor: 'euiColorVis1' },
|
||||
node: { type: 'kubernetesNode', euiVarColor: 'euiColorVis3' },
|
||||
pod: { type: 'kubernetesPod', euiVarColor: 'euiColorVis9' },
|
||||
containerImage: { type: 'container', euiVarColor: 'euiColorVis8' },
|
||||
};
|
||||
|
||||
export const addTreeNavSelectionToFilterQuery = (
|
||||
filterQuery: string | undefined,
|
||||
treeNavSelection: Partial<KubernetesCollectionMap>
|
||||
) => {
|
||||
let validFilterQuery = DEFAULT_FILTER_QUERY;
|
||||
|
||||
try {
|
||||
const parsedFilterQuery: QueryDslQueryContainerBool = JSON.parse(filterQuery || '{}');
|
||||
if (!(parsedFilterQuery?.bool?.filter && Array.isArray(parsedFilterQuery.bool.filter))) {
|
||||
throw new Error('Invalid filter query');
|
||||
}
|
||||
|
||||
parsedFilterQuery.bool.filter.push(
|
||||
...Object.entries(treeNavSelection)
|
||||
.filter(([key]) => (key as KubernetesCollection) !== 'clusterName')
|
||||
.map((obj) => {
|
||||
const [key, value] = obj as [KubernetesCollection, string];
|
||||
|
||||
return {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
[KUBERNETES_COLLECTION_FIELDS[key]]: value,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
validFilterQuery = JSON.stringify(parsedFilterQuery);
|
||||
} catch {
|
||||
// no-op since validFilterQuery is initialized to be DEFAULT_FILTER_QUERY
|
||||
}
|
||||
|
||||
return validFilterQuery;
|
||||
};
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import type { KubernetesCollectionMap } from '../../types';
|
||||
import { LOCAL_STORAGE_TREE_NAV_KEY } from '../../../common/constants';
|
||||
import { addTimerangeAndDefaultFilterToQuery } from '../../utils/add_timerange_and_default_filter_to_query';
|
||||
import { addTreeNavSelectionToFilterQuery } from './helpers';
|
||||
import { IndexPattern, GlobalFilter } from '../../types';
|
||||
|
||||
export type UseTreeViewProps = {
|
||||
globalFilter: GlobalFilter;
|
||||
indexPattern?: IndexPattern;
|
||||
};
|
||||
|
||||
export const useTreeView = ({ globalFilter, indexPattern }: UseTreeViewProps) => {
|
||||
const [noResults, setNoResults] = useState(false);
|
||||
const [treeNavSelection = {}, setTreeNavSelection] = useLocalStorage<
|
||||
Partial<KubernetesCollectionMap>
|
||||
>(LOCAL_STORAGE_TREE_NAV_KEY, {});
|
||||
const filterQueryWithTimeRange = useMemo(() => {
|
||||
return JSON.parse(
|
||||
addTimerangeAndDefaultFilterToQuery(
|
||||
globalFilter.filterQuery,
|
||||
globalFilter.startDate,
|
||||
globalFilter.endDate
|
||||
)
|
||||
);
|
||||
}, [globalFilter.filterQuery, globalFilter.startDate, globalFilter.endDate]);
|
||||
|
||||
const onTreeNavSelect = useCallback(
|
||||
(selection: Partial<KubernetesCollectionMap>) => {
|
||||
setTreeNavSelection(selection);
|
||||
},
|
||||
[setTreeNavSelection]
|
||||
);
|
||||
|
||||
const sessionViewFilter = useMemo(
|
||||
() => addTreeNavSelectionToFilterQuery(globalFilter.filterQuery, treeNavSelection),
|
||||
[globalFilter.filterQuery, treeNavSelection]
|
||||
);
|
||||
|
||||
return {
|
||||
noResults,
|
||||
setNoResults,
|
||||
filterQueryWithTimeRange,
|
||||
indexPattern: indexPattern?.title || '',
|
||||
onTreeNavSelect,
|
||||
treeNavSelection,
|
||||
setTreeNavSelection,
|
||||
sessionViewFilter,
|
||||
};
|
||||
};
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { TreeViewContainer } from '.';
|
||||
import { DEFAULT_FILTER_QUERY } from '../../../common/constants';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import * as context from './contexts';
|
||||
|
||||
describe('TreeNav component', () => {
|
||||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
const spy = jest.spyOn(context, 'useTreeViewContext');
|
||||
|
||||
const defaultProps = {
|
||||
globalFilter: {
|
||||
filterQuery: DEFAULT_FILTER_QUERY,
|
||||
startDate: Date.now().toString(),
|
||||
endDate: (Date.now() + 1).toString(),
|
||||
},
|
||||
renderSessionsView: () => <div>Session View</div>,
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
});
|
||||
afterEach(() => {
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('shows empty message when there is no results', () => {
|
||||
spy.mockImplementation(() => ({
|
||||
...jest.requireActual('./contexts').useTreeViewContext,
|
||||
noResults: true,
|
||||
treeNavSelection: {},
|
||||
}));
|
||||
|
||||
renderResult = mockedContext.render(<TreeViewContainer {...defaultProps} />);
|
||||
expect(renderResult.getByText(/no results/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiSplitPanel, EuiText } from '@elastic/eui';
|
||||
import { useStyles } from './styles';
|
||||
import { IndexPattern, GlobalFilter } from '../../types';
|
||||
import { TreeNav } from './tree_nav';
|
||||
import { Breadcrumb } from './breadcrumb';
|
||||
import { TreeViewContextProvider, useTreeViewContext } from './contexts';
|
||||
import { EmptyState } from './empty_state';
|
||||
|
||||
export interface TreeViewContainerComponentDeps {
|
||||
renderSessionsView: (sessionsFilterQuery: string | undefined) => JSX.Element;
|
||||
}
|
||||
export interface TreeViewContainerDeps extends TreeViewContainerComponentDeps {
|
||||
globalFilter: GlobalFilter;
|
||||
indexPattern?: IndexPattern;
|
||||
}
|
||||
|
||||
export const TreeViewContainer = ({
|
||||
globalFilter,
|
||||
renderSessionsView,
|
||||
indexPattern,
|
||||
}: TreeViewContainerDeps) => {
|
||||
return (
|
||||
<TreeViewContextProvider indexPattern={indexPattern} globalFilter={globalFilter}>
|
||||
<TreeViewContainerComponent renderSessionsView={renderSessionsView} />
|
||||
</TreeViewContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const TreeViewContainerComponent = ({ renderSessionsView }: TreeViewContainerComponentDeps) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const { treeNavSelection, sessionViewFilter, onTreeNavSelect, noResults } = useTreeViewContext();
|
||||
|
||||
return (
|
||||
<EuiSplitPanel.Outer direction="row" hasBorder borderRadius="m" css={styles.outerPanel}>
|
||||
{noResults && <EmptyState />}
|
||||
<EuiSplitPanel.Inner hidden={noResults} color="subdued" grow={false} css={styles.navPanel}>
|
||||
<EuiText>
|
||||
<TreeNav />
|
||||
</EuiText>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner hidden={noResults} css={styles.sessionsPanel}>
|
||||
<Breadcrumb treeNavSelection={treeNavSelection} onSelect={onTreeNavSelect} />
|
||||
{renderSessionsView(sessionViewFilter)}
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
);
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const clusterResponseMock = {
|
||||
buckets: [
|
||||
{
|
||||
key: ['awp-demo-gke-main', 'awp-demo-gke-main'],
|
||||
key_as_string: 'awp-demo-gke-main|awp-demo-gke-main',
|
||||
doc_count: 22279,
|
||||
},
|
||||
{
|
||||
key: ['awp-demo-gke-test', ''],
|
||||
key_as_string: 'awp-demo-gke-test|',
|
||||
doc_count: 1,
|
||||
},
|
||||
],
|
||||
hasNextPage: false,
|
||||
};
|
||||
|
||||
export const nodeResponseMock = {
|
||||
buckets: [
|
||||
{ key: 'default', doc_count: 236 },
|
||||
{ key: 'kube-system', doc_count: 30360 },
|
||||
{ key: 'production', doc_count: 30713 },
|
||||
{ key: 'qa', doc_count: 412 },
|
||||
{ key: 'staging', doc_count: 220 },
|
||||
],
|
||||
hasNextPage: false,
|
||||
};
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { CSSObject } from '@emotion/react';
|
||||
import { useEuiTheme } from '../../hooks';
|
||||
|
||||
export const useStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const cached = useMemo(() => {
|
||||
const { border } = euiTheme;
|
||||
|
||||
const outerPanel: CSSObject = {
|
||||
minHeight: '262px',
|
||||
};
|
||||
|
||||
const navPanel: CSSObject = {
|
||||
borderRight: border.thin,
|
||||
};
|
||||
|
||||
const sessionsPanel: CSSObject = {
|
||||
overflowX: 'auto',
|
||||
};
|
||||
|
||||
return {
|
||||
outerPanel,
|
||||
navPanel,
|
||||
sessionsPanel,
|
||||
};
|
||||
}, [euiTheme]);
|
||||
|
||||
return cached;
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const BREADCRUMBS_CLUSTER_TREE_VIEW_LEVELS = {
|
||||
clusterId: i18n.translate('xpack.kubernetesSecurity.treeView.breadcrumb.clusterId', {
|
||||
defaultMessage: 'Cluster',
|
||||
}),
|
||||
clusterName: i18n.translate('xpack.kubernetesSecurity.treeView.breadcrumb.clusterName', {
|
||||
defaultMessage: 'Cluster',
|
||||
}),
|
||||
namespace: i18n.translate('xpack.kubernetesSecurity.treeView.breadcrumb.namespace', {
|
||||
defaultMessage: 'Namespace',
|
||||
}),
|
||||
node: i18n.translate('xpack.kubernetesSecurity.treeView.breadcrumb.node', {
|
||||
defaultMessage: 'Node',
|
||||
}),
|
||||
pod: i18n.translate('xpack.kubernetesSecurity.treeView.breadcrumb.pod', {
|
||||
defaultMessage: 'Pod',
|
||||
}),
|
||||
containerImage: i18n.translate('xpack.kubernetesSecurity.treeView.breadcrumb.containerImage', {
|
||||
defaultMessage: 'Container Image',
|
||||
}),
|
||||
};
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DynamicTree } from '../../../types';
|
||||
import { KUBERNETES_COLLECTION_FIELDS, KUBERNETES_COLLECTION_ICONS_PROPS } from '../helpers';
|
||||
import { translations } from './translations';
|
||||
|
||||
const LOGICAL_TREE_VIEW: DynamicTree[] = [
|
||||
{
|
||||
key: KUBERNETES_COLLECTION_FIELDS.clusterId,
|
||||
iconProps: KUBERNETES_COLLECTION_ICONS_PROPS.clusterId,
|
||||
type: 'clusterId',
|
||||
name: translations.cluster(),
|
||||
namePlural: translations.cluster(true),
|
||||
},
|
||||
{
|
||||
key: KUBERNETES_COLLECTION_FIELDS.namespace,
|
||||
iconProps: KUBERNETES_COLLECTION_ICONS_PROPS.namespace,
|
||||
type: 'namespace',
|
||||
name: translations.namespace(),
|
||||
namePlural: translations.namespace(true),
|
||||
},
|
||||
{
|
||||
key: KUBERNETES_COLLECTION_FIELDS.pod,
|
||||
iconProps: KUBERNETES_COLLECTION_ICONS_PROPS.pod,
|
||||
type: 'pod',
|
||||
name: translations.pod(),
|
||||
namePlural: translations.pod(true),
|
||||
},
|
||||
{
|
||||
key: KUBERNETES_COLLECTION_FIELDS.containerImage,
|
||||
iconProps: KUBERNETES_COLLECTION_ICONS_PROPS.containerImage,
|
||||
type: 'containerImage',
|
||||
name: translations.containerImage(),
|
||||
namePlural: translations.containerImage(true),
|
||||
},
|
||||
];
|
||||
|
||||
const INFRASTRUCTURE_TREE_VIEW: DynamicTree[] = LOGICAL_TREE_VIEW.map((tree, index) => {
|
||||
if (index === 1) {
|
||||
return {
|
||||
key: KUBERNETES_COLLECTION_FIELDS.node,
|
||||
iconProps: KUBERNETES_COLLECTION_ICONS_PROPS.node,
|
||||
type: 'node',
|
||||
name: translations.node(),
|
||||
namePlural: translations.node(true),
|
||||
};
|
||||
}
|
||||
return tree;
|
||||
});
|
||||
|
||||
export const TREE_VIEW = {
|
||||
logical: LOGICAL_TREE_VIEW,
|
||||
infrastructure: INFRASTRUCTURE_TREE_VIEW,
|
||||
};
|
||||
|
||||
export const INFRASTRUCTURE = 'infrastructure';
|
||||
export const LOGICAL = 'logical';
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../../test';
|
||||
import { clusterResponseMock } from '../mocks';
|
||||
import { TreeNav } from '.';
|
||||
import { TreeViewContextProvider } from '../contexts';
|
||||
|
||||
describe('TreeNav component', () => {
|
||||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
let mockedApi: AppContextTestRender['coreStart']['http']['get'];
|
||||
|
||||
const defaultProps = {
|
||||
globalFilter: {
|
||||
startDate: Date.now().toString(),
|
||||
endDate: (Date.now() + 1).toString(),
|
||||
},
|
||||
onSelect: () => {},
|
||||
hasSelection: false,
|
||||
};
|
||||
|
||||
const TreeNavContainer = () => (
|
||||
<TreeViewContextProvider {...defaultProps}>
|
||||
<TreeNav />
|
||||
</TreeViewContextProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
mockedApi = mockedContext.coreStart.http.get;
|
||||
mockedApi.mockResolvedValue(clusterResponseMock);
|
||||
});
|
||||
|
||||
it('mount with Logical View selected by default', async () => {
|
||||
renderResult = mockedContext.render(<TreeNavContainer />);
|
||||
const elemLabel = await renderResult.getByTestId('treeNavType_generated-idlogical');
|
||||
expect(elemLabel).toHaveAttribute('aria-pressed', 'true');
|
||||
});
|
||||
|
||||
it('shows the tree path according with the selected view type', async () => {
|
||||
renderResult = mockedContext.render(<TreeNavContainer />);
|
||||
|
||||
const logicalViewPath = 'cluster / namespace / pod / container image';
|
||||
const logicViewButton = renderResult.getByTestId('treeNavType_generated-idlogical');
|
||||
expect(logicViewButton).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(renderResult.getByText(logicalViewPath)).toBeInTheDocument();
|
||||
|
||||
const infraStructureViewRadio = renderResult.getByTestId(
|
||||
'treeNavType_generated-idinfrastructure'
|
||||
);
|
||||
infraStructureViewRadio.click();
|
||||
|
||||
expect(renderResult.getByText('cluster / node / pod / container image')).toBeInTheDocument();
|
||||
|
||||
logicViewButton.click();
|
||||
expect(renderResult.getByText(logicalViewPath)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('collapses / expands the tree nav when clicking on collapse button', async () => {
|
||||
renderResult = mockedContext.render(<TreeNavContainer />);
|
||||
|
||||
expect(renderResult.getByText(/cluster/i)).toBeVisible();
|
||||
|
||||
const collapseButton = await renderResult.getByLabelText(/collapse/i);
|
||||
collapseButton.click();
|
||||
expect(renderResult.getByText(/cluster/i)).not.toBeVisible();
|
||||
|
||||
const expandButton = await renderResult.getByLabelText(/expand/i);
|
||||
expandButton.click();
|
||||
expect(renderResult.getByText(/cluster/i)).toBeVisible();
|
||||
});
|
||||
});
|
|
@ -1,137 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
EuiButtonGroup,
|
||||
useGeneratedHtmlId,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
TREE_VIEW_INFRASTRUCTURE_VIEW,
|
||||
TREE_VIEW_LOGICAL_VIEW,
|
||||
TREE_VIEW_SWITCHER_LEGEND,
|
||||
TREE_NAVIGATION_COLLAPSE,
|
||||
TREE_NAVIGATION_EXPAND,
|
||||
} from '../../../../common/translations';
|
||||
import { useStyles } from './styles';
|
||||
import { DynamicTreeView } from '../dynamic_tree_view';
|
||||
import { INFRASTRUCTURE, LOGICAL, TREE_VIEW } from './constants';
|
||||
import { TreeViewKind, TreeViewOptionsGroup } from './types';
|
||||
import { useTreeViewContext } from '../contexts';
|
||||
|
||||
export const TreeNav = () => {
|
||||
const styles = useStyles();
|
||||
const [tree, setTree] = useState(TREE_VIEW.logical);
|
||||
const { filterQueryWithTimeRange, onTreeNavSelect, treeNavSelection, setTreeNavSelection } =
|
||||
useTreeViewContext();
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const treeNavTypePrefix = useGeneratedHtmlId({
|
||||
prefix: 'treeNavType',
|
||||
});
|
||||
const logicalTreeViewPrefix = `${treeNavTypePrefix}${LOGICAL}`;
|
||||
const [toggleIdSelected, setToggleIdSelected] = useState(logicalTreeViewPrefix);
|
||||
|
||||
const selected = useMemo(() => {
|
||||
return Object.entries(treeNavSelection)
|
||||
.map(([k, v]) => `${k}.${v}`)
|
||||
.join();
|
||||
}, [treeNavSelection]);
|
||||
|
||||
const handleToggleCollapse = () => {
|
||||
setIsCollapsed(!isCollapsed);
|
||||
};
|
||||
|
||||
const options: TreeViewOptionsGroup[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: logicalTreeViewPrefix,
|
||||
label: TREE_VIEW_LOGICAL_VIEW,
|
||||
value: LOGICAL,
|
||||
},
|
||||
{
|
||||
id: `${treeNavTypePrefix}${INFRASTRUCTURE}`,
|
||||
label: TREE_VIEW_INFRASTRUCTURE_VIEW,
|
||||
value: INFRASTRUCTURE,
|
||||
},
|
||||
],
|
||||
[logicalTreeViewPrefix, treeNavTypePrefix]
|
||||
);
|
||||
|
||||
const handleTreeViewSwitch = useCallback(
|
||||
(id: string, value: TreeViewKind) => {
|
||||
setToggleIdSelected(id);
|
||||
setTree(TREE_VIEW[value]);
|
||||
setTreeNavSelection({});
|
||||
},
|
||||
[setTreeNavSelection]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCollapsed && (
|
||||
<EuiToolTip content={TREE_NAVIGATION_EXPAND}>
|
||||
<EuiButtonIcon
|
||||
onClick={handleToggleCollapse}
|
||||
iconType="menuRight"
|
||||
aria-label={TREE_NAVIGATION_EXPAND}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
<div style={{ display: isCollapsed ? 'none' : 'inherit' }}>
|
||||
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonGroup
|
||||
name="coarsness"
|
||||
legend={TREE_VIEW_SWITCHER_LEGEND}
|
||||
options={options}
|
||||
idSelected={toggleIdSelected}
|
||||
onChange={handleTreeViewSwitch}
|
||||
buttonSize="compressed"
|
||||
isFullWidth
|
||||
color="primary"
|
||||
css={styles.treeViewSwitcher}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={TREE_NAVIGATION_COLLAPSE}>
|
||||
<EuiButtonIcon
|
||||
onClick={handleToggleCollapse}
|
||||
iconType="menuLeft"
|
||||
aria-label={TREE_NAVIGATION_COLLAPSE}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText color="subdued" size="xs" css={styles.treeViewLegend}>
|
||||
{tree.map((t) => t.name).join(' / ')}
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<div css={styles.treeViewContainer} className="eui-scrollBar">
|
||||
<DynamicTreeView
|
||||
query={filterQueryWithTimeRange}
|
||||
tree={tree}
|
||||
selected={selected}
|
||||
onSelect={(selectionDepth, type, key, clusterName) => {
|
||||
const newSelectionDepth = {
|
||||
...selectionDepth,
|
||||
[type]: key,
|
||||
...(clusterName && { clusterName }),
|
||||
};
|
||||
onTreeNavSelect(newSelectionDepth);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { CSSObject } from '@emotion/react';
|
||||
import { useEuiTheme } from '../../../hooks';
|
||||
|
||||
export const useStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const cached = useMemo(() => {
|
||||
const { size } = euiTheme;
|
||||
|
||||
const treeViewSwitcher: CSSObject = {
|
||||
'.euiButton__text': {
|
||||
fontSize: size.m,
|
||||
},
|
||||
};
|
||||
|
||||
const treeViewContainer: CSSObject = {
|
||||
height: '600px',
|
||||
width: '288px',
|
||||
overflowY: 'auto',
|
||||
};
|
||||
|
||||
const treeViewLegend: CSSObject = {
|
||||
textTransform: 'capitalize',
|
||||
};
|
||||
|
||||
return {
|
||||
treeViewSwitcher,
|
||||
treeViewContainer,
|
||||
treeViewLegend,
|
||||
};
|
||||
}, [euiTheme]);
|
||||
|
||||
return cached;
|
||||
};
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const TREE_NAV_CLUSTER = (isPlural = false) =>
|
||||
i18n.translate('xpack.kubernetesSecurity.treeNav.cluster', {
|
||||
defaultMessage: '{isPlural, select, true {clusters} other {cluster}}',
|
||||
values: { isPlural },
|
||||
});
|
||||
const TREE_NAV_NAMESPACE = (isPlural = false) =>
|
||||
i18n.translate('xpack.kubernetesSecurity.treeNav.namespace', {
|
||||
defaultMessage: '{isPlural, select, true {namespaces} other {namespace}}',
|
||||
values: { isPlural },
|
||||
});
|
||||
const TREE_NAV_POD = (isPlural = false) =>
|
||||
i18n.translate('xpack.kubernetesSecurity.treeNav.pod', {
|
||||
defaultMessage: '{isPlural, select, true {pods} other {pod}}',
|
||||
values: { isPlural },
|
||||
});
|
||||
const TREE_NAV_CONTAINER_IMAGE = (isPlural = false) =>
|
||||
i18n.translate('xpack.kubernetesSecurity.treeNav.containerImage', {
|
||||
defaultMessage: '{isPlural, select, true {container images} other { container image}}',
|
||||
values: { isPlural },
|
||||
});
|
||||
const TREE_NAV_NODE = (isPlural = false) =>
|
||||
i18n.translate('xpack.kubernetesSecurity.treeNav.node', {
|
||||
defaultMessage: '{isPlural, select, true {nodes} other {node}}',
|
||||
values: { isPlural },
|
||||
});
|
||||
|
||||
export const translations = {
|
||||
cluster: TREE_NAV_CLUSTER,
|
||||
namespace: TREE_NAV_NAMESPACE,
|
||||
pod: TREE_NAV_POD,
|
||||
containerImage: TREE_NAV_CONTAINER_IMAGE,
|
||||
node: TREE_NAV_NODE,
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export type TreeViewKind = 'infrastructure' | 'logical';
|
||||
|
||||
export interface TreeViewOptionsGroup {
|
||||
id: string;
|
||||
label: string;
|
||||
value: TreeViewKind;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { useEuiTheme } from '../../hooks';
|
||||
import { TreeViewIconProps } from '../../types';
|
||||
|
||||
export const TreeViewIcon = ({ euiVarColor, ...props }: TreeViewIconProps) => {
|
||||
const { euiVars } = useEuiTheme();
|
||||
|
||||
const colorStyle = euiVars[euiVarColor] ? { style: { color: euiVars[euiVarColor] } } : {};
|
||||
|
||||
return <EuiIcon {...props} {...colorStyle} />;
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { useEuiTheme } from './use_eui_theme';
|
||||
export { useSetFilter } from './use_filter';
|
||||
export { useLastUpdated } from './use_last_updated';
|
||||
export { useScroll } from './use_scroll';
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shade, useEuiTheme as useEuiThemeHook } from '@elastic/eui';
|
||||
import { euiLightVars, euiDarkVars } from '@kbn/ui-theme';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type EuiThemeProps = Parameters<typeof useEuiThemeHook>;
|
||||
type ExtraEuiVars = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
euiColorVis6_asText: string;
|
||||
buttonsBackgroundNormalDefaultPrimary: string;
|
||||
};
|
||||
type EuiVars = typeof euiLightVars & ExtraEuiVars;
|
||||
type EuiThemeReturn = ReturnType<typeof useEuiThemeHook> & { euiVars: EuiVars };
|
||||
|
||||
export type EuiVarsColors = Pick<
|
||||
ReturnType<typeof useEuiTheme>['euiVars'],
|
||||
'euiColorVis0' | 'euiColorVis1' | 'euiColorVis3' | 'euiColorVis8' | 'euiColorVis9'
|
||||
>;
|
||||
// Not all Eui Tokens were fully migrated to @elastic/eui/useEuiTheme yet, so
|
||||
// this hook overrides the default useEuiTheme hook to provide a custom hook that
|
||||
// allows the use the euiVars tokens from the euiLightVars and euiDarkVars
|
||||
export const useEuiTheme = (...props: EuiThemeProps): EuiThemeReturn => {
|
||||
const euiThemeHook = useEuiThemeHook(...props);
|
||||
|
||||
const euiVars = useMemo(() => {
|
||||
const themeVars = euiThemeHook.colorMode === 'DARK' ? euiDarkVars : euiLightVars;
|
||||
|
||||
const extraEuiVars: ExtraEuiVars = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
euiColorVis6_asText: shade(themeVars.euiColorVis6, 0.335),
|
||||
buttonsBackgroundNormalDefaultPrimary: '#006DE4',
|
||||
};
|
||||
|
||||
return {
|
||||
...themeVars,
|
||||
...extraEuiVars,
|
||||
};
|
||||
}, [euiThemeHook.colorMode]);
|
||||
|
||||
return {
|
||||
...euiThemeHook,
|
||||
euiVars,
|
||||
};
|
||||
};
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { StartPlugins } from '../types';
|
||||
|
||||
export const useSetFilter = () => {
|
||||
const { data, timelines } = useKibana<CoreStart & StartPlugins>().services;
|
||||
const { getFilterForValueButton, getFilterOutValueButton, getCopyButton } =
|
||||
timelines.getHoverActions();
|
||||
|
||||
const filterManager = useMemo(() => data.query.filterManager, [data.query.filterManager]);
|
||||
|
||||
return {
|
||||
getFilterForValueButton,
|
||||
getFilterOutValueButton,
|
||||
getCopyButton,
|
||||
filterManager,
|
||||
};
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { GlobalFilter, StartPlugins } from '../types';
|
||||
|
||||
export const useLastUpdated = (globalFilter: GlobalFilter) => {
|
||||
const { timelines: timelinesUi } = useKibana<CoreStart & StartPlugins>().services;
|
||||
|
||||
// Only reset updated at on refresh or after globalFilter gets updated
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const updatedAt = useMemo(() => Date.now(), [globalFilter]);
|
||||
|
||||
return timelinesUi.getLastUpdated({
|
||||
updatedAt: updatedAt || Date.now(),
|
||||
});
|
||||
};
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
const SCROLL_END_BUFFER_HEIGHT = 20;
|
||||
const DEBOUNCE_TIMEOUT = 500;
|
||||
|
||||
function getScrollPosition(div: HTMLElement) {
|
||||
if (div) {
|
||||
return div.scrollTop;
|
||||
} else {
|
||||
return document.documentElement.scrollTop || document.body.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
interface IUseScrollDeps {
|
||||
div: HTMLElement | null;
|
||||
handler(pos: number, endReached: boolean): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* listens to scroll events on given div, if scroll reaches bottom, calls a callback
|
||||
* @param {ref} ref to listen to scroll events on
|
||||
* @param {function} handler function receives params (scrollTop, endReached)
|
||||
*/
|
||||
export function useScroll({ div, handler }: IUseScrollDeps) {
|
||||
useEffect(() => {
|
||||
if (div) {
|
||||
const debounced = _.debounce(() => {
|
||||
const pos = getScrollPosition(div);
|
||||
const endReached = pos + div.offsetHeight > div.scrollHeight - SCROLL_END_BUFFER_HEIGHT;
|
||||
|
||||
handler(pos, endReached);
|
||||
}, DEBOUNCE_TIMEOUT);
|
||||
|
||||
div.onscroll = debounced;
|
||||
|
||||
return () => {
|
||||
debounced.cancel();
|
||||
|
||||
div.onscroll = null;
|
||||
};
|
||||
}
|
||||
}, [div, handler]);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KubernetesSecurityPlugin } from './plugin';
|
||||
|
||||
export type { KubernetesSecurityStart } from './types';
|
||||
export { KUBERNETES_TITLE, KUBERNETES_PATH } from '../common/constants';
|
||||
|
||||
export function plugin() {
|
||||
return new KubernetesSecurityPlugin();
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { KubernetesSecurityDeps } from '../types';
|
||||
|
||||
// Initializing react-query
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const KubernetesSecurityLazy = lazy(() => import('../components/kubernetes_security_routes'));
|
||||
|
||||
export const getKubernetesSecurityLazy = (props: KubernetesSecurityDeps) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<KubernetesSecurityLazy {...props} />
|
||||
</Suspense>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { KubernetesSecurityDeps, KubernetesSecurityServices } from './types';
|
||||
import { getKubernetesSecurityLazy } from './methods';
|
||||
|
||||
export type { KubernetesSecurityStart } from './types';
|
||||
|
||||
export class KubernetesSecurityPlugin implements Plugin {
|
||||
public setup(core: CoreSetup<KubernetesSecurityServices, void>) {}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
return {
|
||||
getKubernetesPage: (kubernetesSecurityDeps: KubernetesSecurityDeps) =>
|
||||
getKubernetesSecurityLazy(kubernetesSecurityDeps),
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, ReactNode, useMemo } from 'react';
|
||||
import { createMemoryHistory, MemoryHistory } from 'history';
|
||||
import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Router } from '@kbn/shared-ux-router';
|
||||
import { History } from 'history';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
|
||||
type UiRender = (ui: React.ReactNode, options?: RenderOptions) => RenderResult;
|
||||
|
||||
/**
|
||||
* Mocked app root context renderer
|
||||
*/
|
||||
export interface AppContextTestRender {
|
||||
history: ReturnType<typeof createMemoryHistory>;
|
||||
coreStart: ReturnType<typeof coreMock.createStart>;
|
||||
/**
|
||||
* A wrapper around `AppRootContext` component. Uses the mocked modules as input to the
|
||||
* `AppRootContext`
|
||||
*/
|
||||
AppWrapper: React.FC<any>;
|
||||
/**
|
||||
* Renders the given UI within the created `AppWrapper` providing the given UI a mocked
|
||||
* endpoint runtime context environment
|
||||
*/
|
||||
render: UiRender;
|
||||
}
|
||||
|
||||
const createCoreStartMock = (
|
||||
history: MemoryHistory<never>
|
||||
): ReturnType<typeof coreMock.createStart> => {
|
||||
const coreStart = coreMock.createStart({ basePath: '/mock' });
|
||||
|
||||
// Mock the certain APP Ids returned by `application.getUrlForApp()`
|
||||
coreStart.application.getUrlForApp.mockImplementation((appId) => {
|
||||
switch (appId) {
|
||||
case 'sessionView':
|
||||
return '/app/sessionView';
|
||||
default:
|
||||
return `${appId} not mocked!`;
|
||||
}
|
||||
});
|
||||
|
||||
coreStart.application.navigateToUrl.mockImplementation((url) => {
|
||||
history.push(url.replace('/app/sessionView', ''));
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
return coreStart;
|
||||
};
|
||||
|
||||
const AppRootProvider = memo<{
|
||||
history: History;
|
||||
coreStart: CoreStart;
|
||||
children: ReactNode | ReactNode[];
|
||||
}>(({ history, coreStart: { http, notifications, theme, application }, children }) => {
|
||||
const isDarkMode = useMemo(() => theme.getTheme().darkMode, [theme]);
|
||||
const services = useMemo(
|
||||
() => ({ http, notifications, application }),
|
||||
[application, http, notifications]
|
||||
);
|
||||
return (
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={services}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<Router history={history}>{children}</Router>
|
||||
</EuiThemeProvider>
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
});
|
||||
|
||||
AppRootProvider.displayName = 'AppRootProvider';
|
||||
|
||||
/**
|
||||
* Creates a mocked app context custom renderer that can be used to render
|
||||
* component that depend upon the application's surrounding context providers.
|
||||
* Factory also returns the content that was used to create the custom renderer, allowing
|
||||
* for further customization.
|
||||
*/
|
||||
|
||||
export const createAppRootMockRenderer = (): AppContextTestRender => {
|
||||
const history = createMemoryHistory<never>();
|
||||
const coreStart = createCoreStartMock(history);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// turns retries off
|
||||
retry: false,
|
||||
// prevent jest did not exit errors
|
||||
cacheTime: Infinity,
|
||||
},
|
||||
},
|
||||
// hide react-query output in console
|
||||
logger: {
|
||||
error: () => {},
|
||||
// eslint-disable-next-line no-console
|
||||
log: console.log,
|
||||
// eslint-disable-next-line no-console
|
||||
warn: console.warn,
|
||||
},
|
||||
});
|
||||
|
||||
const AppWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<AppRootProvider history={history} coreStart={coreStart}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</AppRootProvider>
|
||||
);
|
||||
|
||||
const render: UiRender = (ui, options = {}) => {
|
||||
return reactRender(ui, {
|
||||
wrapper: AppWrapper,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
history,
|
||||
coreStart,
|
||||
AppWrapper,
|
||||
render,
|
||||
};
|
||||
};
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { FieldSpec } from '@kbn/data-plugin/common';
|
||||
import type { TimelinesUIStart } from '@kbn/timelines-plugin/public';
|
||||
import type { SessionViewStart } from '@kbn/session-view-plugin/public';
|
||||
import { EuiIconProps } from '@elastic/eui';
|
||||
import { BoolQuery } from '@kbn/es-query';
|
||||
import { EuiVarsColors } from './hooks/use_eui_theme';
|
||||
|
||||
export interface StartPlugins {
|
||||
data: DataPublicPluginStart;
|
||||
timelines: TimelinesUIStart;
|
||||
sessionView: SessionViewStart;
|
||||
}
|
||||
|
||||
export type KubernetesSecurityServices = CoreStart & StartPlugins;
|
||||
|
||||
export interface IndexPattern {
|
||||
fields: FieldSpec[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface GlobalFilter {
|
||||
filterQuery?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
export interface KubernetesSecurityDeps {
|
||||
filter: React.ReactNode;
|
||||
renderSessionsView: (sessionsFilterQuery: string | undefined) => JSX.Element;
|
||||
indexPattern?: IndexPattern;
|
||||
globalFilter: GlobalFilter;
|
||||
dataViewId?: string;
|
||||
}
|
||||
|
||||
export interface KubernetesSecurityStart {
|
||||
getKubernetesPage: (kubernetesSecurityDeps: KubernetesSecurityDeps) => JSX.Element;
|
||||
}
|
||||
|
||||
export type QueryDslQueryContainerBool = {
|
||||
bool: BoolQuery;
|
||||
};
|
||||
|
||||
export type KubernetesCollection =
|
||||
| 'clusterId'
|
||||
| 'clusterName'
|
||||
| 'namespace'
|
||||
| 'node'
|
||||
| 'pod'
|
||||
| 'containerImage';
|
||||
|
||||
export enum KubernetesTreeViewLevels {
|
||||
clusterId = 'clusterId',
|
||||
clusterName = 'clusterName',
|
||||
namespace = 'namespace',
|
||||
node = 'node',
|
||||
pod = 'pod',
|
||||
containerImage = 'containerImage',
|
||||
}
|
||||
export type KubernetesCollectionMap<T = string> = Record<KubernetesCollection, T>;
|
||||
|
||||
export type TreeViewIconProps = {
|
||||
euiVarColor: keyof EuiVarsColors;
|
||||
} & EuiIconProps;
|
||||
|
||||
export type DynamicTree = {
|
||||
key: string;
|
||||
type: KubernetesCollection;
|
||||
iconProps: TreeViewIconProps;
|
||||
name: string;
|
||||
namePlural: string;
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { addCommasToNumber } from './add_commas_to_number';
|
||||
|
||||
describe('addCommasToNumber(num)', () => {
|
||||
it('works for a number without needing a comma', () => {
|
||||
expect(addCommasToNumber(123)).toEqual('123');
|
||||
});
|
||||
it('works for a number that needs a comma', () => {
|
||||
expect(addCommasToNumber(1234)).toEqual('1,234');
|
||||
});
|
||||
it('works for a number that needs multiple commas', () => {
|
||||
expect(addCommasToNumber(123456789)).toEqual('123,456,789');
|
||||
});
|
||||
it('works for negative number', () => {
|
||||
expect(addCommasToNumber(-10)).toEqual('-10');
|
||||
});
|
||||
it('works for negative number with commas', () => {
|
||||
expect(addCommasToNumber(-10000)).toEqual('-10,000');
|
||||
});
|
||||
it('works for NaN', () => {
|
||||
expect(addCommasToNumber(NaN)).toEqual('NaN');
|
||||
});
|
||||
it('works for Infinity', () => {
|
||||
expect(addCommasToNumber(Infinity)).toEqual('Infinity');
|
||||
});
|
||||
it('works for zero', () => {
|
||||
expect(addCommasToNumber(0)).toEqual('0');
|
||||
});
|
||||
});
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add commas as thousands separators to a number.
|
||||
*
|
||||
* @param {Number} num
|
||||
* @return {String} num in string with commas as thousands separaters
|
||||
*/
|
||||
export function addCommasToNumber(num: number) {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DEFAULT_FILTER_QUERY } from '../../common/constants';
|
||||
import { addTimerangeAndDefaultFilterToQuery } from './add_timerange_and_default_filter_to_query';
|
||||
|
||||
const TEST_QUERY =
|
||||
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"process.entry_leader.same_as_process":true}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}';
|
||||
const TEST_INVALID_QUERY = '{"bool":{"must":[';
|
||||
const TEST_EMPTY_STRING = '';
|
||||
const TEST_DATE = '2022-06-09T22:36:46.628Z';
|
||||
const VALID_RESULT =
|
||||
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"exists":{"field":"orchestrator.cluster.id"}}],"minimum_should_match":1}},{"bool":{"should":[{"match":{"process.entry_leader.same_as_process":true}}],"minimum_should_match":1}},{"range":{"@timestamp":{"gte":"2022-06-09T22:36:46.628Z","lte":"2022-06-09T22:36:46.628Z"}}}],"should":[],"must_not":[]}}';
|
||||
|
||||
describe('addTimerangeAndDefaultFilterToQuery(query, startDate, endDate)', () => {
|
||||
it('works for valid query, startDate, and endDate', () => {
|
||||
expect(addTimerangeAndDefaultFilterToQuery(TEST_QUERY, TEST_DATE, TEST_DATE)).toEqual(
|
||||
VALID_RESULT
|
||||
);
|
||||
});
|
||||
it('works with missing filter in bool', () => {
|
||||
expect(addTimerangeAndDefaultFilterToQuery('{"bool":{}}', TEST_DATE, TEST_DATE)).toEqual(
|
||||
'{"bool":{"filter":[{"range":{"@timestamp":{"gte":"2022-06-09T22:36:46.628Z","lte":"2022-06-09T22:36:46.628Z"}}}]}}'
|
||||
);
|
||||
});
|
||||
it('returns default query with invalid JSON query', () => {
|
||||
expect(addTimerangeAndDefaultFilterToQuery(TEST_INVALID_QUERY, TEST_DATE, TEST_DATE)).toEqual(
|
||||
DEFAULT_FILTER_QUERY
|
||||
);
|
||||
expect(addTimerangeAndDefaultFilterToQuery(TEST_EMPTY_STRING, TEST_DATE, TEST_DATE)).toEqual(
|
||||
DEFAULT_FILTER_QUERY
|
||||
);
|
||||
expect(addTimerangeAndDefaultFilterToQuery('{}', TEST_DATE, TEST_DATE)).toEqual(
|
||||
DEFAULT_FILTER_QUERY
|
||||
);
|
||||
});
|
||||
it('returns default query with invalid startDate or endDate', () => {
|
||||
expect(addTimerangeAndDefaultFilterToQuery(TEST_QUERY, TEST_EMPTY_STRING, TEST_DATE)).toEqual(
|
||||
DEFAULT_FILTER_QUERY
|
||||
);
|
||||
expect(addTimerangeAndDefaultFilterToQuery(TEST_QUERY, TEST_DATE, TEST_EMPTY_STRING)).toEqual(
|
||||
DEFAULT_FILTER_QUERY
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DEFAULT_FILTER, DEFAULT_FILTER_QUERY } from '../../common/constants';
|
||||
|
||||
/**
|
||||
* Add DEFAULT_FILTER and startDate and endDate filter for '@timestamp' field into query.
|
||||
*
|
||||
* Used by frontend components
|
||||
*
|
||||
* @param {String | undefined} query Example: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}'
|
||||
* @param {String} startDate Example: '2022-06-08T18:52:15.532Z'
|
||||
* @param {String} endDate Example: '2022-06-09T17:52:15.532Z'
|
||||
* @return {String} Add startDate and endDate as a '@timestamp' range filter in query and return.
|
||||
* If startDate or endDate is invalid Date string, or that query is not
|
||||
* in the right format, return a default query.
|
||||
*/
|
||||
|
||||
export const addTimerangeAndDefaultFilterToQuery = (
|
||||
query: string | undefined,
|
||||
startDate: string,
|
||||
endDate: string
|
||||
) => {
|
||||
if (!(query && !isNaN(Date.parse(startDate)) && !isNaN(Date.parse(endDate)))) {
|
||||
return DEFAULT_FILTER_QUERY;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedQuery = JSON.parse(query);
|
||||
if (!parsedQuery.bool) {
|
||||
throw new Error("Field 'bool' does not exist in query.");
|
||||
}
|
||||
|
||||
const range = {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: startDate,
|
||||
lte: endDate,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (parsedQuery.bool.filter) {
|
||||
parsedQuery.bool.filter = [DEFAULT_FILTER, ...parsedQuery.bool.filter, range];
|
||||
} else {
|
||||
parsedQuery.bool.filter = [range];
|
||||
}
|
||||
|
||||
return JSON.stringify(parsedQuery);
|
||||
} catch {
|
||||
return DEFAULT_FILTER_QUERY;
|
||||
}
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from '@kbn/core/server';
|
||||
|
||||
export async function plugin(initializerContext: PluginInitializerContext) {
|
||||
const { KubernetesSecurityPlugin } = await import('./plugin');
|
||||
return new KubernetesSecurityPlugin(initializerContext);
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
Plugin,
|
||||
Logger,
|
||||
PluginInitializerContext,
|
||||
IRouter,
|
||||
} from '@kbn/core/server';
|
||||
import { KubernetesSecuritySetupPlugins, KubernetesSecurityStartPlugins } from './types';
|
||||
import { registerRoutes } from './routes';
|
||||
|
||||
export class KubernetesSecurityPlugin implements Plugin {
|
||||
private logger: Logger;
|
||||
private router: IRouter | undefined;
|
||||
|
||||
/**
|
||||
* Initialize KubernetesSecurityPlugin class properties (logger, etc) that is accessible
|
||||
* through the initializerContext.
|
||||
*/
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, plugins: KubernetesSecuritySetupPlugins) {
|
||||
this.logger.debug('kubernetes security: Setup');
|
||||
this.router = core.http.createRouter();
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: KubernetesSecurityStartPlugins) {
|
||||
this.logger.debug('kubernetes security: Start');
|
||||
|
||||
// Register server routes
|
||||
if (this.router) {
|
||||
registerRoutes(this.router, this.logger, plugins.ruleRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.logger.debug('kubernetes security: Stop');
|
||||
}
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { SortCombinations } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { IRouter, Logger } from '@kbn/core/server';
|
||||
import {
|
||||
AGGREGATE_ROUTE,
|
||||
AGGREGATE_PAGE_SIZE,
|
||||
AGGREGATE_MAX_BUCKETS,
|
||||
ORCHESTRATOR_CLUSTER_ID,
|
||||
ORCHESTRATOR_RESOURCE_ID,
|
||||
ORCHESTRATOR_NAMESPACE,
|
||||
ORCHESTRATOR_CLUSTER_NAME,
|
||||
CONTAINER_IMAGE_NAME,
|
||||
CLOUD_INSTANCE_NAME,
|
||||
ENTRY_LEADER_ENTITY_ID,
|
||||
ENTRY_LEADER_USER_ID,
|
||||
ENTRY_LEADER_INTERACTIVE,
|
||||
} from '../../common/constants';
|
||||
import { AggregateBucketPaginationResult } from '../../common/types';
|
||||
|
||||
// sort by values
|
||||
const ASC = 'asc';
|
||||
const DESC = 'desc';
|
||||
|
||||
export const registerAggregateRoute = (router: IRouter, logger: Logger) => {
|
||||
router.versioned
|
||||
.get({
|
||||
access: 'internal',
|
||||
path: AGGREGATE_ROUTE,
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution'],
|
||||
},
|
||||
},
|
||||
validate: {
|
||||
request: {
|
||||
query: schema.object({
|
||||
index: schema.string(),
|
||||
query: schema.string(),
|
||||
countBy: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal(ORCHESTRATOR_CLUSTER_ID),
|
||||
schema.literal(ORCHESTRATOR_RESOURCE_ID),
|
||||
schema.literal(ORCHESTRATOR_NAMESPACE),
|
||||
schema.literal(ORCHESTRATOR_CLUSTER_NAME),
|
||||
schema.literal(CLOUD_INSTANCE_NAME),
|
||||
schema.literal(CONTAINER_IMAGE_NAME),
|
||||
schema.literal(ENTRY_LEADER_ENTITY_ID),
|
||||
])
|
||||
),
|
||||
groupBy: schema.oneOf([
|
||||
schema.literal(ORCHESTRATOR_CLUSTER_ID),
|
||||
schema.literal(ORCHESTRATOR_RESOURCE_ID),
|
||||
schema.literal(ORCHESTRATOR_NAMESPACE),
|
||||
schema.literal(ORCHESTRATOR_CLUSTER_NAME),
|
||||
schema.literal(CLOUD_INSTANCE_NAME),
|
||||
schema.literal(CONTAINER_IMAGE_NAME),
|
||||
schema.literal(ENTRY_LEADER_USER_ID),
|
||||
schema.literal(ENTRY_LEADER_INTERACTIVE),
|
||||
]),
|
||||
page: schema.number({ defaultValue: 0, max: 10000, min: 0 }),
|
||||
perPage: schema.maybe(schema.number({ defaultValue: 10, max: 100, min: 1 })),
|
||||
sortByCount: schema.maybe(schema.oneOf([schema.literal(ASC), schema.literal(DESC)])),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const client = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const { query, countBy, sortByCount, groupBy, page, perPage, index } = request.query;
|
||||
|
||||
try {
|
||||
const body = await doSearch(
|
||||
client,
|
||||
index,
|
||||
query,
|
||||
groupBy,
|
||||
page,
|
||||
perPage,
|
||||
countBy,
|
||||
sortByCount
|
||||
);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
logger.error(`Failed to fetch k8s aggregates: ${err}`);
|
||||
|
||||
return response.customError({
|
||||
body: { message: error.message },
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const doSearch = async (
|
||||
client: ElasticsearchClient,
|
||||
index: string,
|
||||
query: string,
|
||||
groupBy: string,
|
||||
page: number, // zero based
|
||||
perPage = AGGREGATE_PAGE_SIZE,
|
||||
countBy?: string,
|
||||
sortByCount?: string
|
||||
): Promise<AggregateBucketPaginationResult> => {
|
||||
const queryDSL = JSON.parse(query);
|
||||
|
||||
const countByAggs = countBy
|
||||
? {
|
||||
count_by_aggs: {
|
||||
cardinality: {
|
||||
field: countBy,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
let sort: SortCombinations = { _key: { order: ASC } };
|
||||
if (sortByCount === ASC || sortByCount === DESC) {
|
||||
sort = { 'count_by_aggs.value': { order: sortByCount } };
|
||||
}
|
||||
|
||||
const search = await client.search({
|
||||
index: [index],
|
||||
body: {
|
||||
query: queryDSL,
|
||||
size: 0,
|
||||
aggs: {
|
||||
custom_agg: {
|
||||
terms: {
|
||||
field: groupBy,
|
||||
size: AGGREGATE_MAX_BUCKETS,
|
||||
},
|
||||
aggs: {
|
||||
...countByAggs,
|
||||
bucket_sort: {
|
||||
bucket_sort: {
|
||||
sort: [sort], // defaulting to alphabetic sort
|
||||
size: perPage + 1, // check if there's a "next page"
|
||||
from: perPage * page,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const agg: any = search.aggregations?.custom_agg;
|
||||
const buckets = agg?.buckets || [];
|
||||
|
||||
const hasNextPage = buckets.length > perPage;
|
||||
|
||||
if (hasNextPage) {
|
||||
buckets.pop();
|
||||
}
|
||||
|
||||
return {
|
||||
buckets,
|
||||
hasNextPage,
|
||||
};
|
||||
};
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { IRouter, Logger } from '@kbn/core/server';
|
||||
import {
|
||||
COUNT_ROUTE,
|
||||
ORCHESTRATOR_CLUSTER_ID,
|
||||
ORCHESTRATOR_RESOURCE_ID,
|
||||
ORCHESTRATOR_NAMESPACE,
|
||||
ORCHESTRATOR_CLUSTER_NAME,
|
||||
CONTAINER_IMAGE_NAME,
|
||||
CLOUD_INSTANCE_NAME,
|
||||
ENTRY_LEADER_ENTITY_ID,
|
||||
} from '../../common/constants';
|
||||
|
||||
export const registerCountRoute = (router: IRouter, logger: Logger) => {
|
||||
router.versioned
|
||||
.get({
|
||||
access: 'internal',
|
||||
path: COUNT_ROUTE,
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution'],
|
||||
},
|
||||
},
|
||||
validate: {
|
||||
request: {
|
||||
query: schema.object({
|
||||
index: schema.string(),
|
||||
query: schema.string(),
|
||||
field: schema.oneOf([
|
||||
schema.literal(ORCHESTRATOR_CLUSTER_ID),
|
||||
schema.literal(ORCHESTRATOR_RESOURCE_ID),
|
||||
schema.literal(ORCHESTRATOR_NAMESPACE),
|
||||
schema.literal(ORCHESTRATOR_CLUSTER_NAME),
|
||||
schema.literal(CLOUD_INSTANCE_NAME),
|
||||
schema.literal(CONTAINER_IMAGE_NAME),
|
||||
schema.literal(CONTAINER_IMAGE_NAME),
|
||||
schema.literal(ENTRY_LEADER_ENTITY_ID),
|
||||
]),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const client = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const { query, field, index } = request.query;
|
||||
|
||||
try {
|
||||
const body = await doCount(client, index, query, field);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
logger.error(`Failed to fetch k8s counts: ${err}`);
|
||||
|
||||
return response.customError({
|
||||
body: { message: error.message },
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const doCount = async (
|
||||
client: ElasticsearchClient,
|
||||
index: string,
|
||||
query: string,
|
||||
field: string
|
||||
) => {
|
||||
const queryDSL = JSON.parse(query);
|
||||
|
||||
const search = await client.search({
|
||||
index: [index],
|
||||
body: {
|
||||
query: queryDSL,
|
||||
size: 0,
|
||||
aggs: {
|
||||
custom_count: {
|
||||
cardinality: {
|
||||
field,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const agg: any = search.aggregations?.custom_count;
|
||||
|
||||
return agg?.value || 0;
|
||||
};
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { IRouter, Logger } from '@kbn/core/server';
|
||||
import { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
|
||||
import { registerAggregateRoute } from './aggregate';
|
||||
import { registerCountRoute } from './count';
|
||||
import { registerMultiTermsAggregateRoute } from './multi_terms_aggregate';
|
||||
|
||||
export const registerRoutes = (
|
||||
router: IRouter,
|
||||
logger: Logger,
|
||||
ruleRegistry: RuleRegistryPluginStartContract
|
||||
) => {
|
||||
registerAggregateRoute(router, logger);
|
||||
registerCountRoute(router, logger);
|
||||
registerMultiTermsAggregateRoute(router, logger);
|
||||
};
|
|
@ -1,165 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { IRouter, Logger } from '@kbn/core/server';
|
||||
import {
|
||||
MULTI_TERMS_AGGREGATE_ROUTE,
|
||||
AGGREGATE_PAGE_SIZE,
|
||||
ORCHESTRATOR_CLUSTER_ID,
|
||||
ORCHESTRATOR_RESOURCE_ID,
|
||||
ORCHESTRATOR_NAMESPACE,
|
||||
ORCHESTRATOR_CLUSTER_NAME,
|
||||
CONTAINER_IMAGE_NAME,
|
||||
CLOUD_INSTANCE_NAME,
|
||||
ENTRY_LEADER_ENTITY_ID,
|
||||
ENTRY_LEADER_USER_ID,
|
||||
ENTRY_LEADER_INTERACTIVE,
|
||||
} from '../../common/constants';
|
||||
import {
|
||||
MultiTermsAggregateGroupBy,
|
||||
MultiTermsAggregateBucketPaginationResult,
|
||||
} from '../../common/types';
|
||||
|
||||
export const registerMultiTermsAggregateRoute = (router: IRouter, logger: Logger) => {
|
||||
router.versioned
|
||||
.get({
|
||||
access: 'internal',
|
||||
path: MULTI_TERMS_AGGREGATE_ROUTE,
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution'],
|
||||
},
|
||||
},
|
||||
validate: {
|
||||
request: {
|
||||
query: schema.object({
|
||||
index: schema.string(),
|
||||
query: schema.string(),
|
||||
countBy: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal(ORCHESTRATOR_CLUSTER_ID),
|
||||
schema.literal(ORCHESTRATOR_RESOURCE_ID),
|
||||
schema.literal(ORCHESTRATOR_NAMESPACE),
|
||||
schema.literal(ORCHESTRATOR_CLUSTER_NAME),
|
||||
schema.literal(CLOUD_INSTANCE_NAME),
|
||||
schema.literal(CONTAINER_IMAGE_NAME),
|
||||
schema.literal(ENTRY_LEADER_ENTITY_ID),
|
||||
])
|
||||
),
|
||||
groupBys: schema.arrayOf(
|
||||
schema.object({
|
||||
field: schema.oneOf([
|
||||
schema.literal(ORCHESTRATOR_CLUSTER_ID),
|
||||
schema.literal(ORCHESTRATOR_RESOURCE_ID),
|
||||
schema.literal(ORCHESTRATOR_NAMESPACE),
|
||||
schema.literal(ORCHESTRATOR_CLUSTER_NAME),
|
||||
schema.literal(CLOUD_INSTANCE_NAME),
|
||||
schema.literal(CONTAINER_IMAGE_NAME),
|
||||
schema.literal(ENTRY_LEADER_USER_ID),
|
||||
schema.literal(ENTRY_LEADER_INTERACTIVE),
|
||||
]),
|
||||
missing: schema.maybe(schema.string()),
|
||||
}),
|
||||
{ defaultValue: [] }
|
||||
),
|
||||
page: schema.number({ max: 10000, min: 0 }),
|
||||
perPage: schema.maybe(schema.number({ max: 100, min: 1 })),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const client = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const { query, countBy, groupBys, page, perPage, index } = request.query;
|
||||
|
||||
try {
|
||||
const body = await doSearch(client, index, query, groupBys, page, perPage, countBy);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
logger.error(`Failed to fetch k8s multi_terms_aggregate: ${err}`);
|
||||
|
||||
return response.customError({
|
||||
body: { message: error.message },
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const doSearch = async (
|
||||
client: ElasticsearchClient,
|
||||
index: string,
|
||||
query: string,
|
||||
groupBys: MultiTermsAggregateGroupBy[],
|
||||
page: number, // zero based
|
||||
perPage = AGGREGATE_PAGE_SIZE,
|
||||
countBy?: string
|
||||
): Promise<MultiTermsAggregateBucketPaginationResult> => {
|
||||
const queryDSL = JSON.parse(query);
|
||||
|
||||
const countByAggs = countBy
|
||||
? {
|
||||
count_by_aggs: {
|
||||
cardinality: {
|
||||
field: countBy,
|
||||
},
|
||||
},
|
||||
count_alerts: {
|
||||
cardinality: {
|
||||
field: countBy,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const search = await client.search({
|
||||
index: [index],
|
||||
body: {
|
||||
query: queryDSL,
|
||||
size: 0,
|
||||
aggs: {
|
||||
custom_agg: {
|
||||
multi_terms: {
|
||||
terms: groupBys,
|
||||
},
|
||||
aggs: {
|
||||
...countByAggs,
|
||||
bucket_sort: {
|
||||
bucket_sort: {
|
||||
size: perPage + 1, // check if there's a "next page"
|
||||
from: perPage * page,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const agg: any = search.aggregations?.custom_agg;
|
||||
const buckets = agg?.buckets || [];
|
||||
|
||||
const hasNextPage = buckets.length > perPage;
|
||||
|
||||
if (hasNextPage) {
|
||||
buckets.pop();
|
||||
}
|
||||
|
||||
return {
|
||||
buckets,
|
||||
hasNextPage,
|
||||
};
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
RuleRegistryPluginSetupContract as RuleRegistryPluginSetup,
|
||||
RuleRegistryPluginStartContract as RuleRegistryPluginStart,
|
||||
} from '@kbn/rule-registry-plugin/server';
|
||||
|
||||
export interface KubernetesSecuritySetupPlugins {
|
||||
ruleRegistry: RuleRegistryPluginSetup;
|
||||
}
|
||||
|
||||
export interface KubernetesSecurityStartPlugins {
|
||||
ruleRegistry: RuleRegistryPluginStart;
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
},
|
||||
"include": [
|
||||
// add all the folders containg files to be compiled
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"server/**/*.json",
|
||||
"scripts/**/*",
|
||||
"package.json",
|
||||
"storybook/**/*",
|
||||
"../../../../../typings/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
// add references to other TypeScript projects the plugin depends on
|
||||
|
||||
// requiredPlugins from ./kibana.json
|
||||
"@kbn/data-plugin",
|
||||
|
||||
// optionalPlugins from ./kibana.json
|
||||
|
||||
// requiredBundles from ./kibana.json
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/rule-registry-plugin",
|
||||
"@kbn/session-view-plugin",
|
||||
"@kbn/i18n",
|
||||
"@kbn/timelines-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/securitysolution-es-utils",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
}
|
|
@ -32,7 +32,6 @@
|
|||
"features",
|
||||
"fieldFormats",
|
||||
"inspector",
|
||||
"kubernetesSecurity",
|
||||
"lens",
|
||||
"licensing",
|
||||
"maps",
|
||||
|
|
|
@ -14,7 +14,6 @@ import { getTrailingBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../..
|
|||
import { getTrailingBreadcrumbs as geExceptionsBreadcrumbs } from '../../../../exceptions/utils/breadcrumbs';
|
||||
import { getTrailingBreadcrumbs as getCSPBreadcrumbs } from '../../../../cloud_security_posture/breadcrumbs';
|
||||
import { getTrailingBreadcrumbs as getUsersBreadcrumbs } from '../../../../explore/users/pages/details/breadcrumbs';
|
||||
import { getTrailingBreadcrumbs as getKubernetesBreadcrumbs } from '../../../../kubernetes/pages/utils/breadcrumbs';
|
||||
import { getTrailingBreadcrumbs as getDashboardBreadcrumbs } from '../../../../dashboards/pages/breadcrumbs';
|
||||
|
||||
export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = (
|
||||
|
@ -34,8 +33,6 @@ export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = (
|
|||
return getDetectionRulesBreadcrumbs(spyState, getSecuritySolutionUrl);
|
||||
case SecurityPageName.exceptions:
|
||||
return geExceptionsBreadcrumbs(spyState, getSecuritySolutionUrl);
|
||||
case SecurityPageName.kubernetes:
|
||||
return getKubernetesBreadcrumbs(spyState, getSecuritySolutionUrl);
|
||||
case SecurityPageName.cloudSecurityPostureBenchmarks:
|
||||
return getCSPBreadcrumbs(spyState, getSecuritySolutionUrl);
|
||||
case SecurityPageName.dashboards:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue