Remove log stream and settings (#204115)

Removes the code used to render the log stream and settings pages.

The following areas have been checked:
- Log stream embeddable (dashboard of the `cisco_meraki` integration)
- Log stream shared component
- Log categories
- Log anomalies


https://github.com/user-attachments/assets/2bc0763d-3def-4c4b-b50a-21c17976a596

Closes https://github.com/elastic/kibana/issues/204005

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Giorgos Bamparopoulos 2025-01-10 11:51:12 +00:00 committed by GitHub
parent 64b6a1a5e7
commit a3f07db7b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
149 changed files with 17 additions and 10553 deletions

3
.github/CODEOWNERS vendored
View file

@ -1304,7 +1304,6 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql
## Logs UI code exceptions -> @elastic/obs-ux-logs-team
/x-pack/test/api_integration/deployment_agnostic/apis/observability/data_quality/ @elastic/obs-ux-logs-team
/x-pack/test/upgrade/apps/logs @elastic/obs-ux-logs-team
/x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_stream_log_file.ts @elastic/obs-ux-logs-team
/x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_page.ts @elastic/obs-ux-logs-team
/x-pack/solutions/observability/plugins/infra/common/http_api/log_alerts @elastic/obs-ux-logs-team
@ -1419,7 +1418,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql
/x-pack/test/functional/page_objects/dataset_quality.ts @elastic/obs-ux-logs-team
/x-pack/test_serverless/functional/test_suites/observability/index* @elastic/obs-ux-logs-team
/x-pack/test_serverless/functional/test_suites/observability/cypress @elastic/obs-ux-logs-team
/x-pack/test/functional/services/infra_source_configuration_form.ts @elastic/obs-ux-logs-team
/x-pack/test/functional/services/infra_source_configuration_form.ts @elastic/obs-ux-infra_services-team
/x-pack/test/functional/services/logs_ui @elastic/obs-ux-logs-team
/x-pack/test/functional/page_objects/observability_logs_explorer.ts @elastic/obs-ux-logs-team
/test/functional/services/selectable.ts @elastic/obs-ux-logs-team

View file

@ -21965,7 +21965,6 @@
"xpack.infra.analysisSetup.steps.setupProcess.viewResultsButton": "Afficher les résultats",
"xpack.infra.analysisSetup.timeRangeDescription": "Par défaut, le Machine Learning analyse les messages de logs dans vos index de logs n'excédant pas quatre semaines et continue indéfiniment. Vous pouvez spécifier une date de début, de fin ou les deux différente.",
"xpack.infra.analysisSetup.timeRangeTitle": "Choisir une plage temporelle",
"xpack.infra.appName": "Logs Infra",
"xpack.infra.assetDetails.alerts.tooltip.alertsLabel": "Affichage des alertes pour cet hôte. Vous pouvez créer et gérer les alertes dans {alerts}",
"xpack.infra.assetDetails.alerts.tooltip.documentationLabel": "Pour en savoir plus, consultez la {documentation}",
"xpack.infra.assetDetails.alerts.tooltip.documentationLink": "documentation",
@ -22375,7 +22374,6 @@
"xpack.infra.logs.analysis.logEntryCategoriesModuleName": "Catégorisation",
"xpack.infra.logs.analysis.logEntryExamplesViewAnomalyInMlLabel": "Afficher l'anomalie dans le Machine Learning",
"xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel": "Afficher les détails",
"xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel": "Afficher dans le flux",
"xpack.infra.logs.analysis.logEntryRateModuleDescription": "Utilisez le Machine Learning pour détecter automatiquement les taux d'entrées de logs anormaux.",
"xpack.infra.logs.analysis.logEntryRateModuleName": "Taux du log",
"xpack.infra.logs.analysis.manageMlJobsButtonLabel": "Gérer les tâches ML",
@ -22399,23 +22397,9 @@
"xpack.infra.logs.analysisPage.unavailable.mlAppLink": "Application de Machine Learning",
"xpack.infra.logs.anomaliesPageTitle": "Anomalies",
"xpack.infra.logs.categoryExample.viewInContextText": "Afficher en contexte",
"xpack.infra.logs.categoryExample.viewInStreamText": "Afficher dans le flux",
"xpack.infra.logs.common.invalidStateCalloutTitle": "État non valide rencontré",
"xpack.infra.logs.common.invalidStateMessage": "Impossible de traiter l'état {stateValue}.",
"xpack.infra.logs.customizeLogs.customizeButtonLabel": "Personnaliser",
"xpack.infra.logs.customizeLogs.lineWrappingFormRowLabel": "Retour automatique à la ligne",
"xpack.infra.logs.customizeLogs.textSizeFormRowLabel": "Taille du texte",
"xpack.infra.logs.customizeLogs.textSizeRadioGroup": "{textScale, select, small {Petit} medium {Moyen} large {Grand} other {{textScale}} }",
"xpack.infra.logs.customizeLogs.wrapLongLinesSwitchLabel": "Formater les longues lignes",
"xpack.infra.logs.highlights.clearHighlightTermsButtonLabel": "Effacer les termes à mettre en surbrillance",
"xpack.infra.logs.highlights.goToNextHighlightButtonLabel": "Passer au surlignage suivant",
"xpack.infra.logs.highlights.goToPreviousHighlightButtonLabel": "Passer au surlignage précédent",
"xpack.infra.logs.highlights.highlightsPopoverButtonLabel": "Surlignages",
"xpack.infra.logs.highlights.highlightTermsFieldLabel": "Termes à mettre en surbrillance",
"xpack.infra.logs.index.anomaliesTabTitle": "Anomalies des logs",
"xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "Catégories de logs",
"xpack.infra.logs.index.settingsTabTitle": "Paramètres",
"xpack.infra.logs.index.streamTabTitle": "Flux",
"xpack.infra.logs.logCategoriesTitle": "Catégories",
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "Analyse dans ML",
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "Analysez cette catégorie dans l'application ML.",
@ -22451,42 +22435,10 @@
"xpack.infra.logs.noDataConfig.beatsCard.title": "Ajouter une intégration au logging",
"xpack.infra.logs.noDataConfig.solutionName": "Observabilité",
"xpack.infra.logs.pluginTitle": "Logs",
"xpack.infra.logs.search.nextButtonLabel": "Suivant",
"xpack.infra.logs.search.previousButtonLabel": "Précédent",
"xpack.infra.logs.search.searchInLogsAriaLabel": "rechercher",
"xpack.infra.logs.search.searchInLogsPlaceholder": "Recherche",
"xpack.infra.logs.searchResultTooltip": "{bucketCount, plural, one {# entrée mise en surbrillance} other {# entrées mises en surbrillance}}",
"xpack.infra.logs.settings.inlineLogViewCalloutButtonText": "Revenir à la vue par défaut du flux de logs",
"xpack.infra.logs.settings.inlineLogViewCalloutWidgetDescription": "Les modifications seront synchronisées avec l'URL, mais elles ne seront pas conservées dans la vue par défaut du flux de logs.",
"xpack.infra.logs.settings.inlineLogViewCalloutWidgetTitle": "Vous configurez un widget intégré",
"xpack.infra.logs.startStreamingButtonLabel": "Diffuser en direct",
"xpack.infra.logs.stopStreamingButtonLabel": "Arrêter la diffusion",
"xpack.infra.logs.streamPageTitle": "Flux",
"xpack.infra.logs.viewInContext.logsFromContainerTitle": "Les logs affichés proviennent du conteneur {container}",
"xpack.infra.logs.viewInContext.logsFromFileTitle": "Les logs affichés proviennent du fichier {file} et de l'hôte {host}",
"xpack.infra.logsDeprecationCallout.euiCallOut.discoverANewLogLabel": "Il existe une nouvelle (et meilleure) façon d'explorer vos logs !",
"xpack.infra.logsHeaderAddDataButtonLabel": "Ajouter des données",
"xpack.infra.logSourceConfiguration.childFormElementErrorMessage": "L'état d'au moins un champ du formulaire est non valide.",
"xpack.infra.logSourceConfiguration.dataViewDescription": "Vue de données contenant les données de log",
"xpack.infra.logSourceConfiguration.dataViewLabel": "Vue de données de log",
"xpack.infra.logSourceConfiguration.dataViewSectionTitle": "Vue de données (déclassée)",
"xpack.infra.logSourceConfiguration.dataViewSelectorPlaceholder": "Choisir une vue de données",
"xpack.infra.logSourceConfiguration.dataViewsManagementLinkText": "écran de gestion des vues de données",
"xpack.infra.logSourceConfiguration.dataViewTitle": "Vue de données de log",
"xpack.infra.logSourceConfiguration.emptyColumnListErrorMessage": "La liste de colonnes ne doit pas être vide.",
"xpack.infra.logSourceConfiguration.emptyFieldErrorMessage": "Le champ \"{fieldName}\" ne doit pas être vide.",
"xpack.infra.logSourceConfiguration.includesSpacesErrorMessage": "Le champ \"{fieldName}\" ne doit pas contenir despaces.",
"xpack.infra.logSourceConfiguration.invalidMessageFieldTypeErrorMessage": "Le champ {messageField} doit être un champ textuel.",
"xpack.infra.logSourceConfiguration.kibanaAdvancedSettingSectionTitle": "Paramètres avancés des sources de logs Kibana",
"xpack.infra.logSourceConfiguration.logDataViewHelpText": "Les vues de données sont partagées entre les applications dans l'espace Kibana et peuvent être gérées via l{dataViewsManagementLink}. Une vue de données peut cibler plusieurs index.",
"xpack.infra.logSourceConfiguration.logSourceConfigurationFormErrorsCalloutTitle": "Configuration de la source incohérente",
"xpack.infra.logSourceConfiguration.logSourcesTitle": "Sources de log",
"xpack.infra.logSourceConfiguration.missingDataViewErrorMessage": "La vue de données {dataViewId} doit exister.",
"xpack.infra.logSourceConfiguration.missingDataViewsLabel": "Vue de données {indexPatternId} manquante",
"xpack.infra.logSourceConfiguration.missingMessageFieldErrorMessage": "La vue de données doit contenir un champ {messageField}.",
"xpack.infra.logSourceConfiguration.missingTimestampFieldErrorMessage": "La vue de données doit être basée sur le temps.",
"xpack.infra.logSourceConfiguration.rollupIndexPatternErrorMessage": "La vue de données ne doit pas être un modèle d'indexation de cumul.",
"xpack.infra.logSourceConfiguration.unsavedFormPromptMessage": "Voulez-vous vraiment quitter ? Les modifications seront perdues",
"xpack.infra.logSourceErrorPage.failedToLoadSourceMessage": "Des erreurs se sont produites lors du chargement de la configuration. Réessayez ou modifiez la configuration pour résoudre le problème.",
"xpack.infra.logSourceErrorPage.failedToLoadSourceTitle": "Impossible de charger la configuration",
"xpack.infra.logSourceErrorPage.fetchLogSourceConfigurationErrorTitle": "Impossible de charger la configuration de la source de logs",
@ -22495,13 +22447,7 @@
"xpack.infra.logSourceErrorPage.resolveLogSourceConfigurationErrorTitle": "Impossible de résoudre la configuration de la source de logs",
"xpack.infra.logSourceErrorPage.savedObjectNotFoundErrorMessage": "Impossible de localiser ce {savedObjectType} : {savedObjectId}",
"xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "Réessayer",
"xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "Recherche d'entrées de log… (par ex. host.name:host-1)",
"xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "Erreur de filtrage du log",
"xpack.infra.logsSettingsPage.loadingButtonLabel": "Chargement",
"xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "La maintenance des panneaux de flux de logs n'est plus assurée. Essayez d'utiliser {savedSearchDocsLink} pour une visualisation similaire.",
"xpack.infra.logStreamPageTemplate.backtoLogsStream": "Retour au flux de logs",
"xpack.infra.logStreamPageTemplate.widgetBadge": "Widget",
"xpack.infra.logStreamPageTemplate.widgetDescription": "Vous visionnez un widget intégré. Les modifications seront synchronisées avec l'URL, mais elles ne seront pas conservées dans la vue par défaut du flux de logs.",
"xpack.infra.metadata.pinAriaLabel": "Champ épinglé",
"xpack.infra.metadataEmbeddable.AddFilterAriaLabel": "Ajouter un filtre",
"xpack.infra.metadataEmbeddable.errorAction": "récupérer de nouveau les métadonnées",
@ -22856,9 +22802,6 @@
"xpack.infra.ml.aomalyFlyout.jobSetup.flyoutHeader": "Activer le Machine Learning pour {nodeType}",
"xpack.infra.ml.metricsHostModuleDescription": "Utilisez le Machine Learning pour détecter automatiquement les taux d'entrées de logs anormaux.",
"xpack.infra.ml.metricsModuleName": "Détection des anomalies d'indicateurs",
"xpack.infra.ml.splash.inlineLogView.buttonText": "Revenir à la vue de log (persistante) par défaut",
"xpack.infra.ml.splash.inlineLogView.description": "Cette fonctionnalité ne prend pas en charge les vues de journal en ligne.",
"xpack.infra.ml.splash.inlineLogView.title": "Basculer vers la vue de log persistante",
"xpack.infra.ml.splash.loadingMessage": "Vérification de la licence...",
"xpack.infra.ml.splash.startTrialCta": "Démarrer l'essai",
"xpack.infra.ml.splash.startTrialDescription": "Notre essai gratuit inclut les fonctionnalités de Machine Learning, qui vous permettent de détecter les anomalies dans vos logs.",
@ -22949,28 +22892,14 @@
"xpack.infra.savedView.updateView": "Mettre à jour la vue",
"xpack.infra.showHistory": "Afficher l'historique",
"xpack.infra.snapshot.missingSnapshotMetricError": "L'agrégation de {metric} pour {nodeType} n'est pas disponible.",
"xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "Ajouter une colonne",
"xpack.infra.sourceConfiguration.anomalyThresholdDescription": "Définit le score de sévérité minimal requis pour afficher les anomalies dans l'application Metrics.",
"xpack.infra.sourceConfiguration.anomalyThresholdLabel": "Score de sévérité minimal",
"xpack.infra.sourceConfiguration.anomalyThresholdTitle": "Seuil de sévérité d'anomalie",
"xpack.infra.sourceConfiguration.applySettingsButtonLabel": "Appliquer",
"xpack.infra.sourceConfiguration.discardSettingsButtonLabel": "Abandonner",
"xpack.infra.sourceConfiguration.featuresSectionTitle": "Fonctionnalités",
"xpack.infra.sourceConfiguration.fieldContainEmptyEntryErrorMessage": "Le champ ne doit pas inclure de valeurs vides séparées par des virgules.",
"xpack.infra.sourceConfiguration.fieldContainSpacesErrorMessage": "Le champ ne doit pas contenir d'espaces.",
"xpack.infra.sourceConfiguration.fieldEmptyErrorMessage": "Le champ ne doit pas être vide.",
"xpack.infra.sourceConfiguration.fieldLogColumnTitle": "Champ",
"xpack.infra.sourceConfiguration.indicesSectionTitle": "Index",
"xpack.infra.sourceConfiguration.logColumnsSectionTitle": "Colonnes de log",
"xpack.infra.sourceConfiguration.logIndices.viewAffectedRulesLink": "Afficher les règles affectées",
"xpack.infra.sourceConfiguration.logIndicesDescription": "Modèle d'indexation pour la correspondance d'index contenant des données de log",
"xpack.infra.sourceConfiguration.logIndicesLabel": "Index de log",
"xpack.infra.sourceConfiguration.logIndicesRecommendedValue": "La valeur recommandée est {defaultValue}",
"xpack.infra.sourceConfiguration.logIndicesTitle": "Index de log",
"xpack.infra.sourceConfiguration.logsIndicesSectionTitle": "Index (déclassés)",
"xpack.infra.sourceConfiguration.logsIndicesUsedByRulesMessage": "Une ou plusieurs règles dalerte reposent sur ce paramètre de source de données. La modification de ce paramètre modifiera les données utilisées pour générer des alertes.",
"xpack.infra.sourceConfiguration.logsIndicesUsedByRulesTitle": "Les règles d'alerte utilisent ce paramètre de source de données",
"xpack.infra.sourceConfiguration.messageLogColumnDescription": "Ce champ système affiche le message d'entrée de log dérivé des champs de document.",
"xpack.infra.sourceConfiguration.metricIndices.viewAffectedRulesLink": "Afficher les règles affectées",
"xpack.infra.sourceConfiguration.metricIndicesDescription": "Modèle d'indexation pour la correspondance d'index contenant des données d'indicateurs",
"xpack.infra.sourceConfiguration.metricIndicesDoNotExist": "Impossible de trouver des données dindicateurs car le modèle saisi ne correspond à aucun index.",
@ -22985,16 +22914,11 @@
"xpack.infra.sourceConfiguration.nameDescription": "Nom descriptif pour la configuration de la source",
"xpack.infra.sourceConfiguration.nameLabel": "Nom",
"xpack.infra.sourceConfiguration.nameSectionTitle": "Nom",
"xpack.infra.sourceConfiguration.noLogColumnsDescription": "Ajoutez une colonne à cette liste à l'aide du bouton ci-dessus.",
"xpack.infra.sourceConfiguration.noLogColumnsTitle": "Aucune colonne",
"xpack.infra.sourceConfiguration.noRemoteClusterMessage": "Nous ne parvenons pas à nous connecter au cluster distant, ce qui nous empêche de récupérer les indicateurs et les données dont vous avez besoin. Pour résoudre ce problème, vérifiez la configuration de vos index et assurez-vous qu'elle est correctement effectuée.",
"xpack.infra.sourceConfiguration.noRemoteClusterTitle": "Impossible de se connecter au serveur distant",
"xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExist": "Vérifiez que le cluster distant est disponible ou que les paramètres de connexion à distance sont corrects.",
"xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExistTitle": "Impossible de se connecter au cluster distant",
"xpack.infra.sourceConfiguration.removeLogColumnButtonLabel": "Retirer la colonne {columnDescription}",
"xpack.infra.sourceConfiguration.saveButton": "Enregistrer les modifications",
"xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "Système",
"xpack.infra.sourceConfiguration.timestampLogColumnDescription": "Ce champ système affiche l'heure de l'entrée de log telle que déterminée par le paramètre du champ {timestampSetting}.",
"xpack.infra.sourceConfiguration.unsavedFormPrompt": "Voulez-vous vraiment quitter ? Les modifications seront perdues",
"xpack.infra.sourceConfiguration.updateFailureBody": "Nous n'avons pas pu appliquer les modifications à la configuration des indicateurs. Réessayez plus tard.",
"xpack.infra.sourceConfiguration.updateFailureTitle": "La mise à jour de la configuration a échoué",

View file

@ -21826,7 +21826,6 @@
"xpack.infra.analysisSetup.steps.setupProcess.viewResultsButton": "結果を表示",
"xpack.infra.analysisSetup.timeRangeDescription": "デフォルトで、機械学習は 4 週間以内のログインデックスのログメッセージを分析し、永久に継続します。別の開始日、終了日、または両方を指定できます。",
"xpack.infra.analysisSetup.timeRangeTitle": "時間範囲の選択",
"xpack.infra.appName": "インフラログ",
"xpack.infra.assetDetails.alerts.tooltip.alertsLabel": "このホストのアラートを表示しています。{alerts}でアラートを作成および管理できます",
"xpack.infra.assetDetails.alerts.tooltip.documentationLabel": "詳細については、{documentation}を参照してください",
"xpack.infra.assetDetails.alerts.tooltip.documentationLink": "ドキュメンテーション",
@ -22238,7 +22237,6 @@
"xpack.infra.logs.analysis.logEntryCategoriesModuleName": "カテゴリー分け",
"xpack.infra.logs.analysis.logEntryExamplesViewAnomalyInMlLabel": "機械学習で異常を表示",
"xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel": "詳細を表示",
"xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel": "ストリームで表示",
"xpack.infra.logs.analysis.logEntryRateModuleDescription": "機械学習を使用して自動的に異常ログエントリ率を検出します。",
"xpack.infra.logs.analysis.logEntryRateModuleName": "ログレート",
"xpack.infra.logs.analysis.manageMlJobsButtonLabel": "MLジョブの管理",
@ -22262,23 +22260,9 @@
"xpack.infra.logs.analysisPage.unavailable.mlAppLink": "機械学習アプリ",
"xpack.infra.logs.anomaliesPageTitle": "異常",
"xpack.infra.logs.categoryExample.viewInContextText": "コンテキストで表示",
"xpack.infra.logs.categoryExample.viewInStreamText": "ストリームで表示",
"xpack.infra.logs.common.invalidStateCalloutTitle": "無効な状態が発生しました",
"xpack.infra.logs.common.invalidStateMessage": "状態{stateValue}を処理できません。",
"xpack.infra.logs.customizeLogs.customizeButtonLabel": "カスタマイズ",
"xpack.infra.logs.customizeLogs.lineWrappingFormRowLabel": "改行",
"xpack.infra.logs.customizeLogs.textSizeFormRowLabel": "テキストサイズ",
"xpack.infra.logs.customizeLogs.textSizeRadioGroup": "{textScale, select, small {小} Medium {中} Large {大} other {{textScale}} }",
"xpack.infra.logs.customizeLogs.wrapLongLinesSwitchLabel": "長い行を改行",
"xpack.infra.logs.highlights.clearHighlightTermsButtonLabel": "ハイライトする用語をクリア",
"xpack.infra.logs.highlights.goToNextHighlightButtonLabel": "次のハイライトにスキップ",
"xpack.infra.logs.highlights.goToPreviousHighlightButtonLabel": "前のハイライトにスキップ",
"xpack.infra.logs.highlights.highlightsPopoverButtonLabel": "ハイライト",
"xpack.infra.logs.highlights.highlightTermsFieldLabel": "ハイライトする用語",
"xpack.infra.logs.index.anomaliesTabTitle": "Logs異常",
"xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "Logsカテゴリ",
"xpack.infra.logs.index.settingsTabTitle": "設定",
"xpack.infra.logs.index.streamTabTitle": "ストリーム",
"xpack.infra.logs.logCategoriesTitle": "カテゴリー",
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析",
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。",
@ -22314,41 +22298,10 @@
"xpack.infra.logs.noDataConfig.beatsCard.title": "ロギング統合を追加",
"xpack.infra.logs.noDataConfig.solutionName": "Observability",
"xpack.infra.logs.pluginTitle": "ログ",
"xpack.infra.logs.search.nextButtonLabel": "次へ",
"xpack.infra.logs.search.previousButtonLabel": "前へ",
"xpack.infra.logs.search.searchInLogsAriaLabel": "検索",
"xpack.infra.logs.search.searchInLogsPlaceholder": "検索",
"xpack.infra.logs.searchResultTooltip": "{bucketCount, plural, other {# 件のハイライトされたエントリー}}",
"xpack.infra.logs.settings.inlineLogViewCalloutButtonText": "デフォルトログストリームビューに戻す",
"xpack.infra.logs.settings.inlineLogViewCalloutWidgetDescription": "変更はURLと同期されますが、デフォルトログストリームビューには永続しません。",
"xpack.infra.logs.settings.inlineLogViewCalloutWidgetTitle": "埋め込まれたウィジェットを構成しています",
"xpack.infra.logs.startStreamingButtonLabel": "ライブストリーム",
"xpack.infra.logs.stopStreamingButtonLabel": "ストリーム停止",
"xpack.infra.logs.streamPageTitle": "ストリーム",
"xpack.infra.logs.viewInContext.logsFromContainerTitle": "表示されたログはコンテナー{container}から取得されました",
"xpack.infra.logs.viewInContext.logsFromFileTitle": "表示されたログは、ファイル{file}およびホスト{host}から取得されました",
"xpack.infra.logsDeprecationCallout.euiCallOut.discoverANewLogLabel": "ログの探索には、新しく、もっと効果的な方法があります。",
"xpack.infra.logsHeaderAddDataButtonLabel": "データの追加",
"xpack.infra.logSourceConfiguration.childFormElementErrorMessage": "1つ以上のフォームフィールドが無効な状態です。",
"xpack.infra.logSourceConfiguration.dataViewDescription": "ログデータを含むデータビュー",
"xpack.infra.logSourceConfiguration.dataViewLabel": "ログデータビュー",
"xpack.infra.logSourceConfiguration.dataViewSectionTitle": "データビュー(廃止予定)",
"xpack.infra.logSourceConfiguration.dataViewSelectorPlaceholder": "データビューを選択",
"xpack.infra.logSourceConfiguration.dataViewsManagementLinkText": "データビュー管理画面",
"xpack.infra.logSourceConfiguration.dataViewTitle": "ログデータビュー",
"xpack.infra.logSourceConfiguration.emptyColumnListErrorMessage": "列リストは未入力のままにできません。",
"xpack.infra.logSourceConfiguration.emptyFieldErrorMessage": "フィールド''{fieldName}''は未入力のままにできません。",
"xpack.infra.logSourceConfiguration.includesSpacesErrorMessage": "フィールド''{fieldName}''にはスペースを入力できません。",
"xpack.infra.logSourceConfiguration.invalidMessageFieldTypeErrorMessage": "{messageField}フィールドはテキストフィールドでなければなりません。",
"xpack.infra.logSourceConfiguration.kibanaAdvancedSettingSectionTitle": "Kibanaログソース詳細設定",
"xpack.infra.logSourceConfiguration.logSourceConfigurationFormErrorsCalloutTitle": "一貫しないソース構成",
"xpack.infra.logSourceConfiguration.logSourcesTitle": "ログソース",
"xpack.infra.logSourceConfiguration.missingDataViewErrorMessage": "データビュー{dataViewId}が存在する必要があります。",
"xpack.infra.logSourceConfiguration.missingDataViewsLabel": "データビュー{indexPatternId}が見つかりません",
"xpack.infra.logSourceConfiguration.missingMessageFieldErrorMessage": "データビューには{messageField}フィールドが必要です。",
"xpack.infra.logSourceConfiguration.missingTimestampFieldErrorMessage": "データビューは時間に基づく必要があります。",
"xpack.infra.logSourceConfiguration.rollupIndexPatternErrorMessage": "データビューがロールアップインデックスパターンであってはなりません。",
"xpack.infra.logSourceConfiguration.unsavedFormPromptMessage": "終了してよろしいですか?変更内容は失われます",
"xpack.infra.logSourceErrorPage.failedToLoadSourceMessage": "構成の読み込み試行中にエラーが発生しました。再試行するか、構成を変更して問題を修正してください。",
"xpack.infra.logSourceErrorPage.failedToLoadSourceTitle": "構成を読み込めませんでした",
"xpack.infra.logSourceErrorPage.fetchLogSourceConfigurationErrorTitle": "ログソース構成を読み込めませんでした",
@ -22357,13 +22310,7 @@
"xpack.infra.logSourceErrorPage.resolveLogSourceConfigurationErrorTitle": "ログソース構成を解決できませんでした",
"xpack.infra.logSourceErrorPage.savedObjectNotFoundErrorMessage": "{savedObjectType}{savedObjectId}が見つかりませんでした",
"xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "再試行",
"xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "ログエントリーを検索中…host.name:host-1",
"xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "ログフィルターエラー",
"xpack.infra.logsSettingsPage.loadingButtonLabel": "読み込み中",
"xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "ログストリームパネルは管理されていません。{savedSearchDocsLink}を同様の視覚化に活用してください。",
"xpack.infra.logStreamPageTemplate.backtoLogsStream": "ログストリームに戻る",
"xpack.infra.logStreamPageTemplate.widgetBadge": "ウィジェット",
"xpack.infra.logStreamPageTemplate.widgetDescription": "埋め込まれたウィジェットを表示しています。変更はURLと同期されますが、デフォルトログストリームビューには永続しません。",
"xpack.infra.metadata.pinAriaLabel": "固定されたフィールド",
"xpack.infra.metadataEmbeddable.AddFilterAriaLabel": "フィルターを追加",
"xpack.infra.metadataEmbeddable.errorAction": "メタデータを再取得",
@ -22717,9 +22664,6 @@
"xpack.infra.ml.aomalyFlyout.jobSetup.flyoutHeader": "{nodeType}の機械学習を有効にする",
"xpack.infra.ml.metricsHostModuleDescription": "機械学習を使用して自動的に異常ログエントリ率を検出します。",
"xpack.infra.ml.metricsModuleName": "メトリック異常検知",
"xpack.infra.ml.splash.inlineLogView.buttonText": "デフォルト(永続)ログビューに戻す",
"xpack.infra.ml.splash.inlineLogView.description": "この機能はインラインログビューをサポートしていません",
"xpack.infra.ml.splash.inlineLogView.title": "永続ログビューに切り替える",
"xpack.infra.ml.splash.loadingMessage": "ライセンスを確認しています...",
"xpack.infra.ml.splash.startTrialCta": "トライアルを開始",
"xpack.infra.ml.splash.startTrialDescription": "無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。",
@ -22809,28 +22753,14 @@
"xpack.infra.savedView.updateView": "ビューの更新",
"xpack.infra.showHistory": "履歴を表示",
"xpack.infra.snapshot.missingSnapshotMetricError": "{nodeType}の{metric}の集約を使用できません。",
"xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "列を追加",
"xpack.infra.sourceConfiguration.anomalyThresholdDescription": "メトリックアプリケーションで異常値を表示するために必要な最低重要度スコアを設定します。",
"xpack.infra.sourceConfiguration.anomalyThresholdLabel": "最低重要度スコア",
"xpack.infra.sourceConfiguration.anomalyThresholdTitle": "異常重要度しきい値",
"xpack.infra.sourceConfiguration.applySettingsButtonLabel": "適用",
"xpack.infra.sourceConfiguration.discardSettingsButtonLabel": "破棄",
"xpack.infra.sourceConfiguration.featuresSectionTitle": "機能",
"xpack.infra.sourceConfiguration.fieldContainEmptyEntryErrorMessage": "フィールドには空のカンマ区切り値を含めることはできません。",
"xpack.infra.sourceConfiguration.fieldContainSpacesErrorMessage": "フィールドにはスペースを入力できません。",
"xpack.infra.sourceConfiguration.fieldEmptyErrorMessage": "このフィールドは未入力のままにできません。",
"xpack.infra.sourceConfiguration.fieldLogColumnTitle": "フィールド",
"xpack.infra.sourceConfiguration.indicesSectionTitle": "インデックス",
"xpack.infra.sourceConfiguration.logColumnsSectionTitle": "ログ列",
"xpack.infra.sourceConfiguration.logIndices.viewAffectedRulesLink": "影響を受けるルールを表示",
"xpack.infra.sourceConfiguration.logIndicesDescription": "ログデータを含む一致するインデックスのインデックスパターンです",
"xpack.infra.sourceConfiguration.logIndicesLabel": "ログインデックス",
"xpack.infra.sourceConfiguration.logIndicesRecommendedValue": "推奨値は {defaultValue} です",
"xpack.infra.sourceConfiguration.logIndicesTitle": "ログインデックス",
"xpack.infra.sourceConfiguration.logsIndicesSectionTitle": "インデックス(廃止予定)",
"xpack.infra.sourceConfiguration.logsIndicesUsedByRulesMessage": "1つ以上のアラートルールがこのデータソース設定に依存しています。この設定を変更すると、アラートを生成するために使用されるデータが変更されます。",
"xpack.infra.sourceConfiguration.logsIndicesUsedByRulesTitle": "アラートルールはこのデータソース設定を使用します。",
"xpack.infra.sourceConfiguration.messageLogColumnDescription": "このシステムフィールドは、ドキュメントフィールドから取得されたログエントリーメッセージを表示します。",
"xpack.infra.sourceConfiguration.metricIndices.viewAffectedRulesLink": "影響を受けるルールを表示",
"xpack.infra.sourceConfiguration.metricIndicesDescription": "メトリックデータを含む一致するインデックスのインデックスパターンです",
"xpack.infra.sourceConfiguration.metricIndicesDoNotExist": "入力されたパターンがインデックスと一致しないため、メトリックデータを検索できませんでした。",
@ -22845,16 +22775,11 @@
"xpack.infra.sourceConfiguration.nameDescription": "ソース構成を説明する名前です",
"xpack.infra.sourceConfiguration.nameLabel": "名前",
"xpack.infra.sourceConfiguration.nameSectionTitle": "名前",
"xpack.infra.sourceConfiguration.noLogColumnsDescription": "上のボタンでこのリストに列を追加します。",
"xpack.infra.sourceConfiguration.noLogColumnsTitle": "列がありません",
"xpack.infra.sourceConfiguration.noRemoteClusterMessage": "リモートクラスターに接続できません。このため、必要なメトリックとデータを取得できません。この問題を解決するには、インデックス構成を確認し、構成が正しいことを確かめてください。",
"xpack.infra.sourceConfiguration.noRemoteClusterTitle": "リモートクラスターに接続できませんでした",
"xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExist": "リモートクラスターが利用可能であるか、リモート接続の設定が正しいことを確認します。",
"xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExistTitle": "リモートクラスターに接続できませんでした",
"xpack.infra.sourceConfiguration.removeLogColumnButtonLabel": "{columnDescription} 列を削除",
"xpack.infra.sourceConfiguration.saveButton": "変更を保存",
"xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "システム",
"xpack.infra.sourceConfiguration.timestampLogColumnDescription": "このシステムフィールドは、{timestampSetting} フィールド設定から判断されたログエントリーの時刻を表示します。",
"xpack.infra.sourceConfiguration.unsavedFormPrompt": "終了してよろしいですか?変更内容は失われます",
"xpack.infra.sourceConfiguration.updateFailureBody": "変更をメトリック構成に適用できませんでした。しばらくたってから再試行してください。",
"xpack.infra.sourceConfiguration.updateFailureTitle": "構成の更新が失敗しました",

View file

@ -21475,7 +21475,6 @@
"xpack.infra.analysisSetup.steps.setupProcess.viewResultsButton": "查看结果",
"xpack.infra.analysisSetup.timeRangeDescription": "默认情况下Machine Learning 分析日志索引中不超过 4 周的日志消息,并无限持续下去。您可以指定不同的开始日期或/和结束日期。",
"xpack.infra.analysisSetup.timeRangeTitle": "选择时间范围",
"xpack.infra.appName": "基础架构日志",
"xpack.infra.assetDetails.alerts.tooltip.alertsLabel": "正在显示此主机的告警。可以在 {alerts} 中创建和管理告警",
"xpack.infra.assetDetails.alerts.tooltip.documentationLabel": "请参阅{documentation}了解更多信息",
"xpack.infra.assetDetails.alerts.tooltip.documentationLink": "文档",
@ -21879,7 +21878,6 @@
"xpack.infra.logs.analysis.logEntryCategoriesModuleName": "归类",
"xpack.infra.logs.analysis.logEntryExamplesViewAnomalyInMlLabel": "在 Machine Learning 中查看异常",
"xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel": "查看详情",
"xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel": "在流中查看",
"xpack.infra.logs.analysis.logEntryRateModuleDescription": "使用 Machine Learning 自动检测异常日志条目速率。",
"xpack.infra.logs.analysis.logEntryRateModuleName": "日志速率",
"xpack.infra.logs.analysis.manageMlJobsButtonLabel": "管理 ML 作业",
@ -21903,23 +21901,9 @@
"xpack.infra.logs.analysisPage.unavailable.mlAppLink": "Machine Learning 应用",
"xpack.infra.logs.anomaliesPageTitle": "异常",
"xpack.infra.logs.categoryExample.viewInContextText": "在上下文中查看",
"xpack.infra.logs.categoryExample.viewInStreamText": "在流中查看",
"xpack.infra.logs.common.invalidStateCalloutTitle": "遇到无效状态",
"xpack.infra.logs.common.invalidStateMessage": "无法处理状态 {stateValue}。",
"xpack.infra.logs.customizeLogs.customizeButtonLabel": "定制",
"xpack.infra.logs.customizeLogs.lineWrappingFormRowLabel": "换行",
"xpack.infra.logs.customizeLogs.textSizeFormRowLabel": "文本大小",
"xpack.infra.logs.customizeLogs.textSizeRadioGroup": "{textScale, select, small {小} medium {Medium} large {Large} other {{textScale}} }",
"xpack.infra.logs.customizeLogs.wrapLongLinesSwitchLabel": "长行换行",
"xpack.infra.logs.highlights.clearHighlightTermsButtonLabel": "清除要突出显示的词",
"xpack.infra.logs.highlights.goToNextHighlightButtonLabel": "跳转到下一高亮条目",
"xpack.infra.logs.highlights.goToPreviousHighlightButtonLabel": "跳转到上一高亮条目",
"xpack.infra.logs.highlights.highlightsPopoverButtonLabel": "突出显示",
"xpack.infra.logs.highlights.highlightTermsFieldLabel": "要突出显示的词",
"xpack.infra.logs.index.anomaliesTabTitle": "日志异常",
"xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "日志类别",
"xpack.infra.logs.index.settingsTabTitle": "设置",
"xpack.infra.logs.index.streamTabTitle": "流式传输",
"xpack.infra.logs.logCategoriesTitle": "类别",
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析",
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。",
@ -21955,40 +21939,10 @@
"xpack.infra.logs.noDataConfig.beatsCard.title": "添加日志记录集成",
"xpack.infra.logs.noDataConfig.solutionName": "Observability",
"xpack.infra.logs.pluginTitle": "日志",
"xpack.infra.logs.search.nextButtonLabel": "下一步",
"xpack.infra.logs.search.previousButtonLabel": "上一页",
"xpack.infra.logs.search.searchInLogsAriaLabel": "搜索",
"xpack.infra.logs.search.searchInLogsPlaceholder": "搜索",
"xpack.infra.logs.searchResultTooltip": "{bucketCount, plural, other {# 个高亮条目}}",
"xpack.infra.logs.settings.inlineLogViewCalloutButtonText": "恢复为默认日志流视图",
"xpack.infra.logs.settings.inlineLogViewCalloutWidgetDescription": "更改将同步到 URL但不会持续存在于默认日志流视图。",
"xpack.infra.logs.settings.inlineLogViewCalloutWidgetTitle": "您正在配置嵌入式小组件",
"xpack.infra.logs.startStreamingButtonLabel": "实时流式传输",
"xpack.infra.logs.stopStreamingButtonLabel": "停止流式传输",
"xpack.infra.logs.streamPageTitle": "流式传输",
"xpack.infra.logs.viewInContext.logsFromContainerTitle": "显示的日志来自容器 {container}",
"xpack.infra.logs.viewInContext.logsFromFileTitle": "显示的日志来自文件 {file} 和主机 {host}",
"xpack.infra.logsDeprecationCallout.euiCallOut.discoverANewLogLabel": "这是浏览日志的更有效的新方法!",
"xpack.infra.logsHeaderAddDataButtonLabel": "添加数据",
"xpack.infra.logSourceConfiguration.childFormElementErrorMessage": "至少一个表单字段处于无效状态。",
"xpack.infra.logSourceConfiguration.dataViewDescription": "包含日志数据的数据视图",
"xpack.infra.logSourceConfiguration.dataViewLabel": "日志数据视图",
"xpack.infra.logSourceConfiguration.dataViewSectionTitle": "数据视图(已过时)",
"xpack.infra.logSourceConfiguration.dataViewSelectorPlaceholder": "选择数据视图",
"xpack.infra.logSourceConfiguration.dataViewsManagementLinkText": "数据视图管理屏幕",
"xpack.infra.logSourceConfiguration.dataViewTitle": "日志数据视图",
"xpack.infra.logSourceConfiguration.emptyColumnListErrorMessage": "列列表不得为空。",
"xpack.infra.logSourceConfiguration.invalidMessageFieldTypeErrorMessage": "{messageField} 字段必须是文本字段。",
"xpack.infra.logSourceConfiguration.kibanaAdvancedSettingSectionTitle": "Kibana 日志源高级设置",
"xpack.infra.logSourceConfiguration.logDataViewHelpText": "数据视图在 Kibana 工作区中的应用间共享,并可以通过 {dataViewsManagementLink} 进行管理。单一数据视图可以针对多个索引。",
"xpack.infra.logSourceConfiguration.logSourceConfigurationFormErrorsCalloutTitle": "内容配置不一致",
"xpack.infra.logSourceConfiguration.logSourcesTitle": "日志源",
"xpack.infra.logSourceConfiguration.missingDataViewErrorMessage": "数据视图 {dataViewId} 必须存在。",
"xpack.infra.logSourceConfiguration.missingDataViewsLabel": "缺少数据视图 {indexPatternId}",
"xpack.infra.logSourceConfiguration.missingMessageFieldErrorMessage": "数据视图必须包含 {messageField} 字段。",
"xpack.infra.logSourceConfiguration.missingTimestampFieldErrorMessage": "数据视图必须基于时间。",
"xpack.infra.logSourceConfiguration.rollupIndexPatternErrorMessage": "数据视图不得为汇总/打包索引模式。",
"xpack.infra.logSourceConfiguration.unsavedFormPromptMessage": "是否确定要离开?更改将丢失",
"xpack.infra.logSourceErrorPage.failedToLoadSourceMessage": "尝试加载配置时出错。请重试或更改配置以解决问题。",
"xpack.infra.logSourceErrorPage.failedToLoadSourceTitle": "无法加载配置",
"xpack.infra.logSourceErrorPage.fetchLogSourceConfigurationErrorTitle": "无法加载日志源配置",
@ -21997,13 +21951,7 @@
"xpack.infra.logSourceErrorPage.resolveLogSourceConfigurationErrorTitle": "无法解决日志源配置",
"xpack.infra.logSourceErrorPage.savedObjectNotFoundErrorMessage": "无法找到该{savedObjectType}{savedObjectId}",
"xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "重试",
"xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "搜索日志条目……(例如 host.name:host-1",
"xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "日志筛选错误",
"xpack.infra.logsSettingsPage.loadingButtonLabel": "正在加载",
"xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "将不再维护日志流面板。尝试将 {savedSearchDocsLink} 用于类似可视化。",
"xpack.infra.logStreamPageTemplate.backtoLogsStream": "返回到日志流",
"xpack.infra.logStreamPageTemplate.widgetBadge": "小组件",
"xpack.infra.logStreamPageTemplate.widgetDescription": "您正在查看嵌入式小组件。更改将同步到 URL但不会持续存在于默认日志流视图。",
"xpack.infra.metadata.pinAriaLabel": "已固定字段",
"xpack.infra.metadataEmbeddable.AddFilterAriaLabel": "添加筛选",
"xpack.infra.metadataEmbeddable.errorAction": "重新提取元数据",
@ -22355,9 +22303,6 @@
"xpack.infra.ml.aomalyFlyout.jobSetup.flyoutHeader": "为 {nodeType} 启用 Machine Learning",
"xpack.infra.ml.metricsHostModuleDescription": "使用 Machine Learning 自动检测异常日志条目速率。",
"xpack.infra.ml.metricsModuleName": "指标异常检测",
"xpack.infra.ml.splash.inlineLogView.buttonText": "恢复到默认(持久化)日志视图",
"xpack.infra.ml.splash.inlineLogView.description": "此功能不支持内联日志视图",
"xpack.infra.ml.splash.inlineLogView.title": "切换到持久化日志视图",
"xpack.infra.ml.splash.loadingMessage": "正在检查许可证......",
"xpack.infra.ml.splash.startTrialCta": "开始试用",
"xpack.infra.ml.splash.startTrialDescription": "我们的免费试用版包含 Machine Learning 功能,可用于检测日志中的异常。",
@ -22448,28 +22393,14 @@
"xpack.infra.savedView.updateView": "更新视图",
"xpack.infra.showHistory": "显示历史记录",
"xpack.infra.snapshot.missingSnapshotMetricError": "{nodeType} 的 {metric} 聚合不可用。",
"xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "添加列",
"xpack.infra.sourceConfiguration.anomalyThresholdDescription": "设置在 Metrics 应用程序中显示异常所需的最低严重性分数。",
"xpack.infra.sourceConfiguration.anomalyThresholdLabel": "最低严重性分数",
"xpack.infra.sourceConfiguration.anomalyThresholdTitle": "异常严重性阈值",
"xpack.infra.sourceConfiguration.applySettingsButtonLabel": "应用",
"xpack.infra.sourceConfiguration.discardSettingsButtonLabel": "丢弃",
"xpack.infra.sourceConfiguration.featuresSectionTitle": "功能",
"xpack.infra.sourceConfiguration.fieldContainEmptyEntryErrorMessage": "字段不得包含逗号分隔的空值。",
"xpack.infra.sourceConfiguration.fieldContainSpacesErrorMessage": "字段不得包括空格。",
"xpack.infra.sourceConfiguration.fieldEmptyErrorMessage": "字段不得为空。",
"xpack.infra.sourceConfiguration.fieldLogColumnTitle": "字段",
"xpack.infra.sourceConfiguration.indicesSectionTitle": "索引",
"xpack.infra.sourceConfiguration.logColumnsSectionTitle": "日志列",
"xpack.infra.sourceConfiguration.logIndices.viewAffectedRulesLink": "查看受影响的规则",
"xpack.infra.sourceConfiguration.logIndicesDescription": "用于匹配包含日志数据的索引的索引模式",
"xpack.infra.sourceConfiguration.logIndicesLabel": "日志索引",
"xpack.infra.sourceConfiguration.logIndicesRecommendedValue": "推荐值为 {defaultValue}",
"xpack.infra.sourceConfiguration.logIndicesTitle": "日志索引",
"xpack.infra.sourceConfiguration.logsIndicesSectionTitle": "索引(已过时)",
"xpack.infra.sourceConfiguration.logsIndicesUsedByRulesMessage": "一个或多个告警规则依赖于此数据源设置。更改此设置会更改用于生成告警的数据。",
"xpack.infra.sourceConfiguration.logsIndicesUsedByRulesTitle": "告警规则使用此数据源设置",
"xpack.infra.sourceConfiguration.messageLogColumnDescription": "此系统字段显示派生自文档字段的日志条目消息。",
"xpack.infra.sourceConfiguration.metricIndices.viewAffectedRulesLink": "查看受影响的规则",
"xpack.infra.sourceConfiguration.metricIndicesDescription": "用于匹配包含指标数据的索引的索引模式",
"xpack.infra.sourceConfiguration.metricIndicesDoNotExist": "找不到任何指标数据,因为输入的模式不匹配任何索引。",
@ -22484,15 +22415,11 @@
"xpack.infra.sourceConfiguration.nameDescription": "源配置的描述性名称",
"xpack.infra.sourceConfiguration.nameLabel": "名称",
"xpack.infra.sourceConfiguration.nameSectionTitle": "名称",
"xpack.infra.sourceConfiguration.noLogColumnsDescription": "使用上面的按钮将列添加到此列表。",
"xpack.infra.sourceConfiguration.noLogColumnsTitle": "无列",
"xpack.infra.sourceConfiguration.noRemoteClusterMessage": "无法连接到远程集群,这导致我们无法检索您所需的指标和数据。要解决此问题,请检查您的索引配置,并确保进行了正确配置。",
"xpack.infra.sourceConfiguration.noRemoteClusterTitle": "无法连接到远程集群",
"xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExist": "检查远程集群是否可用,或远程连接设置是否正确。",
"xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExistTitle": "无法连接到远程集群",
"xpack.infra.sourceConfiguration.saveButton": "保存更改",
"xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "系统",
"xpack.infra.sourceConfiguration.timestampLogColumnDescription": "此系统字段显示 {timestampSetting} 字段设置所确定的日志条目时间。",
"xpack.infra.sourceConfiguration.unsavedFormPrompt": "是否确定要离开?更改将丢失",
"xpack.infra.sourceConfiguration.updateFailureBody": "无法对指标配置应用更改。请稍后重试。",
"xpack.infra.sourceConfiguration.updateFailureTitle": "配置更新失败",

View file

@ -9,5 +9,4 @@
* Exporting versioned APIs types
*/
export * from './latest';
export * as logEntriesV1 from './log_entries/v1';
export * as logViewsV1 from './log_views/v1';

View file

@ -5,5 +5,4 @@
* 2.0.
*/
export * from './log_entries/v1';
export * from './log_views/v1';

View file

@ -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 * as rt from 'io-ts';
import { logEntryCursorRT, logEntryRT } from '../../../log_entry';
import { logViewColumnConfigurationRT } from '../../../log_views';
import { logViewReferenceRT } from '../../../log_views';
export const LOG_ENTRIES_HIGHLIGHTS_PATH = '/api/log_entries/highlights';
export const logEntriesHighlightsBaseRequestRT = rt.intersection([
rt.type({
logView: logViewReferenceRT,
startTimestamp: rt.number,
endTimestamp: rt.number,
highlightTerms: rt.array(rt.string),
}),
rt.partial({
query: rt.union([rt.string, rt.null]),
size: rt.number,
columns: rt.array(logViewColumnConfigurationRT),
}),
]);
export const logEntriesHighlightsBeforeRequestRT = rt.intersection([
logEntriesHighlightsBaseRequestRT,
rt.type({ before: rt.union([logEntryCursorRT, rt.literal('last')]) }),
]);
export const logEntriesHighlightsAfterRequestRT = rt.intersection([
logEntriesHighlightsBaseRequestRT,
rt.type({ after: rt.union([logEntryCursorRT, rt.literal('first')]) }),
]);
export const logEntriesHighlightsCenteredRequestRT = rt.intersection([
logEntriesHighlightsBaseRequestRT,
rt.type({ center: logEntryCursorRT }),
]);
export const logEntriesHighlightsRequestRT = rt.union([
logEntriesHighlightsBaseRequestRT,
logEntriesHighlightsBeforeRequestRT,
logEntriesHighlightsAfterRequestRT,
logEntriesHighlightsCenteredRequestRT,
]);
export type LogEntriesHighlightsRequest = rt.TypeOf<typeof logEntriesHighlightsRequestRT>;
export const logEntriesHighlightsResponseRT = rt.type({
data: rt.array(
rt.union([
rt.type({
topCursor: rt.null,
bottomCursor: rt.null,
entries: rt.array(logEntryRT),
}),
rt.type({
topCursor: logEntryCursorRT,
bottomCursor: logEntryCursorRT,
entries: rt.array(logEntryRT),
}),
])
),
});
export type LogEntriesHighlightsResponse = rt.TypeOf<typeof logEntriesHighlightsResponseRT>;

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './highlights';
export * from './summary';
export * from './summary_highlights';

View file

@ -1,39 +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 * as rt from 'io-ts';
import { logViewReferenceRT } from '../../../log_views';
export const LOG_ENTRIES_SUMMARY_PATH = '/api/log_entries/summary';
export const logEntriesSummaryRequestRT = rt.type({
logView: logViewReferenceRT,
startTimestamp: rt.number,
endTimestamp: rt.number,
bucketSize: rt.number,
query: rt.union([rt.string, rt.undefined, rt.null]),
});
export type LogEntriesSummaryRequest = rt.TypeOf<typeof logEntriesSummaryRequestRT>;
export const logEntriesSummaryBucketRT = rt.type({
start: rt.number,
end: rt.number,
entriesCount: rt.number,
});
export type LogEntriesSummaryBucket = rt.TypeOf<typeof logEntriesSummaryBucketRT>;
export const logEntriesSummaryResponseRT = rt.type({
data: rt.type({
start: rt.number,
end: rt.number,
buckets: rt.array(logEntriesSummaryBucketRT),
}),
});
export type LogEntriesSummaryResponse = rt.TypeOf<typeof logEntriesSummaryResponseRT>;

View file

@ -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 * as rt from 'io-ts';
import { logEntryCursorRT } from '../../../log_entry';
import { logEntriesSummaryRequestRT, logEntriesSummaryBucketRT } from './summary';
export const LOG_ENTRIES_SUMMARY_HIGHLIGHTS_PATH = '/api/log_entries/summary_highlights';
export const logEntriesSummaryHighlightsRequestRT = rt.intersection([
logEntriesSummaryRequestRT,
rt.type({
highlightTerms: rt.array(rt.string),
}),
]);
export type LogEntriesSummaryHighlightsRequest = rt.TypeOf<
typeof logEntriesSummaryHighlightsRequestRT
>;
export const logEntriesSummaryHighlightsBucketRT = rt.intersection([
logEntriesSummaryBucketRT,
rt.type({
representativeKey: logEntryCursorRT,
}),
]);
export type LogEntriesSummaryHighlightsBucket = rt.TypeOf<
typeof logEntriesSummaryHighlightsBucketRT
>;
export const logEntriesSummaryHighlightsResponseRT = rt.type({
data: rt.array(
rt.type({
start: rt.number,
end: rt.number,
buckets: rt.array(logEntriesSummaryHighlightsBucketRT),
})
),
});
export type LogEntriesSummaryHighlightsResponse = rt.TypeOf<
typeof logEntriesSummaryHighlightsResponseRT
>;

View file

@ -47,19 +47,6 @@ export * from './log_entry';
export { convertISODateToNanoPrecision } from './utils';
// Http types
export type { LogEntriesSummaryBucket, LogEntriesSummaryHighlightsBucket } from './http_api';
// Http runtime
export {
LOG_ENTRIES_HIGHLIGHTS_PATH,
LOG_ENTRIES_SUMMARY_PATH,
logEntriesHighlightsRequestRT,
logEntriesHighlightsResponseRT,
logEntriesSummaryRequestRT,
logEntriesSummaryResponseRT,
} from './http_api';
// Locators
export {
type LogsLocatorParams,

View file

@ -6,7 +6,7 @@
*/
import type { TimeKey } from '@kbn/io-ts-utils';
import { ascending, bisector } from 'd3-array';
import { ascending } from 'd3-array';
export type Comparator = (firstValue: any, secondValue: any) => number;
@ -38,25 +38,3 @@ export const compareToTimeKey =
<Value>(keyAccessor: (value: Value) => TimeKey, compareValues?: Comparator) =>
(value: Value, key: TimeKey) =>
compareTimeKeys(keyAccessor(value), key, compareValues);
export const getIndexAtTimeKey = <Value>(
keyAccessor: (value: Value) => TimeKey,
compareValues?: Comparator
) => {
const comparator = compareToTimeKey(keyAccessor, compareValues);
const collectionBisector = bisector(comparator);
return (collection: Value[], key: TimeKey): number | null => {
const index = collectionBisector.left(collection, key);
if (index >= collection.length) {
return null;
}
if (comparator(collection[index], key) !== 0) {
return null;
}
return index;
};
};

View file

@ -1,30 +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 { HttpHandler } from '@kbn/core/public';
import { decodeOrThrow } from '../../../../../common/runtime_types';
import {
LOG_ENTRIES_HIGHLIGHTS_PATH,
LogEntriesHighlightsRequest,
logEntriesHighlightsRequestRT,
logEntriesHighlightsResponseRT,
} from '../../../../../common/http_api';
export const fetchLogEntriesHighlights = async (
requestArgs: LogEntriesHighlightsRequest,
fetch: HttpHandler
) => {
const response = await fetch(LOG_ENTRIES_HIGHLIGHTS_PATH, {
method: 'POST',
body: JSON.stringify(logEntriesHighlightsRequestRT.encode(requestArgs)),
version: '1',
});
return decodeOrThrow(logEntriesHighlightsResponseRT)(response);
};

View file

@ -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 type { HttpHandler } from '@kbn/core/public';
import { decodeOrThrow } from '../../../../../common/runtime_types';
import {
LOG_ENTRIES_SUMMARY_HIGHLIGHTS_PATH,
LogEntriesSummaryHighlightsRequest,
logEntriesSummaryHighlightsRequestRT,
logEntriesSummaryHighlightsResponseRT,
} from '../../../../../common/http_api';
export const fetchLogSummaryHighlights = async (
requestArgs: LogEntriesSummaryHighlightsRequest,
fetch: HttpHandler
) => {
const response = await fetch(LOG_ENTRIES_SUMMARY_HIGHLIGHTS_PATH, {
method: 'POST',
body: JSON.stringify(logEntriesSummaryHighlightsRequestRT.encode(requestArgs)),
version: '1',
});
return decodeOrThrow(logEntriesSummaryHighlightsResponseRT)(response);
};

View file

@ -1,8 +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 './log_highlights';

View file

@ -1,104 +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 { TimeKey } from '@kbn/io-ts-utils';
import { useEffect, useMemo, useState } from 'react';
import { LogViewReference } from '../../../../common';
import { LogEntriesHighlightsResponse } from '../../../../common/http_api';
import { LogEntry } from '../../../../common/log_entry';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import { fetchLogEntriesHighlights } from './api/fetch_log_entries_highlights';
export const useLogEntryHighlights = (
logViewReference: LogViewReference,
sourceVersion: string | undefined,
startTimestamp: number | null,
endTimestamp: number | null,
centerPoint: TimeKey | null,
size: number,
filterQuery: string | null,
highlightTerms: string[]
) => {
const { services } = useKibanaContextForPlugin();
const [logEntryHighlights, setLogEntryHighlights] = useState<
LogEntriesHighlightsResponse['data']
>([]);
const [loadLogEntryHighlightsRequest, loadLogEntryHighlights] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
if (!startTimestamp || !endTimestamp || !centerPoint || !highlightTerms.length) {
throw new Error('Skipping request: Insufficient parameters');
}
return await fetchLogEntriesHighlights(
{
logView: logViewReference,
startTimestamp,
endTimestamp,
center: centerPoint,
size,
query: filterQuery || undefined,
highlightTerms,
},
services.http.fetch
);
},
onResolve: (response) => {
setLogEntryHighlights(response.data);
},
},
[logViewReference, startTimestamp, endTimestamp, centerPoint, size, filterQuery, highlightTerms]
);
useEffect(() => {
setLogEntryHighlights([]);
}, [highlightTerms]);
useEffect(() => {
if (
highlightTerms.filter((highlightTerm) => highlightTerm.length > 0).length &&
startTimestamp &&
endTimestamp
) {
loadLogEntryHighlights();
} else {
setLogEntryHighlights([]);
}
}, [
endTimestamp,
filterQuery,
highlightTerms,
loadLogEntryHighlights,
sourceVersion,
startTimestamp,
]);
const logEntryHighlightsById = useMemo(
() =>
logEntryHighlights.reduce<Record<string, LogEntry[]>>(
(accumulatedLogEntryHighlightsById, highlightData) => {
return highlightData.entries.reduce((singleHighlightLogEntriesById, entry) => {
const highlightsForId = singleHighlightLogEntriesById[entry.id] || [];
return {
...singleHighlightLogEntriesById,
[entry.id]: [...highlightsForId, entry],
};
}, accumulatedLogEntryHighlightsById);
},
{}
),
[logEntryHighlights]
);
return {
logEntryHighlights,
logEntryHighlightsById,
loadLogEntryHighlightsRequest,
};
};

View file

@ -1,93 +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 createContainer from 'constate';
import { useState } from 'react';
import useThrottle from 'react-use/lib/useThrottle';
import { TimeKey } from '@kbn/io-ts-utils';
import { LogViewReference } from '../../../../common';
import { useLogEntryHighlights } from './log_entry_highlights';
import { useLogSummaryHighlights } from './log_summary_highlights';
import { useNextAndPrevious } from './next_and_previous';
import { useLogPositionStateContext } from '../log_position';
const FETCH_THROTTLE_INTERVAL = 3000;
interface UseLogHighlightsStateProps {
logViewReference: LogViewReference;
sourceVersion: string | undefined;
centerCursor: TimeKey | null;
size: number;
filterQuery: string | null;
}
const useLogHighlightsState = ({
logViewReference,
sourceVersion,
centerCursor,
size,
filterQuery,
}: UseLogHighlightsStateProps) => {
const [highlightTerms, setHighlightTerms] = useState<string[]>([]);
const { visibleMidpoint, jumpToTargetPosition, startTimestamp, endTimestamp } =
useLogPositionStateContext();
const throttledStartTimestamp = useThrottle(startTimestamp, FETCH_THROTTLE_INTERVAL);
const throttledEndTimestamp = useThrottle(endTimestamp, FETCH_THROTTLE_INTERVAL);
const { logEntryHighlights, logEntryHighlightsById, loadLogEntryHighlightsRequest } =
useLogEntryHighlights(
logViewReference,
sourceVersion,
throttledStartTimestamp,
throttledEndTimestamp,
centerCursor,
size,
filterQuery,
highlightTerms
);
const { logSummaryHighlights, loadLogSummaryHighlightsRequest } = useLogSummaryHighlights(
logViewReference,
sourceVersion,
throttledStartTimestamp,
throttledEndTimestamp,
filterQuery,
highlightTerms
);
const {
currentHighlightKey,
hasPreviousHighlight,
hasNextHighlight,
goToPreviousHighlight,
goToNextHighlight,
} = useNextAndPrevious({
visibleMidpoint,
logEntryHighlights,
highlightTerms,
jumpToTargetPosition,
});
return {
highlightTerms,
setHighlightTerms,
logEntryHighlights,
logEntryHighlightsById,
logSummaryHighlights,
loadLogEntryHighlightsRequest,
loadLogSummaryHighlightsRequest,
currentHighlightKey,
hasPreviousHighlight,
hasNextHighlight,
goToPreviousHighlight,
goToNextHighlight,
};
};
export const [LogHighlightsStateProvider, useLogHighlightsStateContext] =
createContainer(useLogHighlightsState);

View file

@ -1,93 +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, useMemo, useState } from 'react';
import { debounce } from 'lodash';
import { LogViewReference } from '../../../../common';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import { fetchLogSummaryHighlights } from './api/fetch_log_summary_highlights';
import { LogEntriesSummaryHighlightsResponse } from '../../../../common/http_api';
import { useBucketSize } from '../log_summary/bucket_size';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
export const useLogSummaryHighlights = (
logViewReference: LogViewReference,
sourceVersion: string | undefined,
startTimestamp: number | null,
endTimestamp: number | null,
filterQuery: string | null,
highlightTerms: string[]
) => {
const { services } = useKibanaContextForPlugin();
const [logSummaryHighlights, setLogSummaryHighlights] = useState<
LogEntriesSummaryHighlightsResponse['data']
>([]);
const bucketSize = useBucketSize(startTimestamp, endTimestamp);
const [loadLogSummaryHighlightsRequest, loadLogSummaryHighlights] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
if (!startTimestamp || !endTimestamp || !bucketSize || !highlightTerms.length) {
throw new Error('Skipping request: Insufficient parameters');
}
return await fetchLogSummaryHighlights(
{
logView: logViewReference,
startTimestamp,
endTimestamp,
bucketSize,
query: filterQuery,
highlightTerms,
},
services.http.fetch
);
},
onResolve: (response) => {
setLogSummaryHighlights(response.data);
},
},
[logViewReference, startTimestamp, endTimestamp, bucketSize, filterQuery, highlightTerms]
);
const debouncedLoadSummaryHighlights = useMemo(
() => debounce(loadLogSummaryHighlights, 275),
[loadLogSummaryHighlights]
);
useEffect(() => {
setLogSummaryHighlights([]);
}, [highlightTerms]);
useEffect(() => {
if (
highlightTerms.filter((highlightTerm) => highlightTerm.length > 0).length &&
startTimestamp &&
endTimestamp
) {
debouncedLoadSummaryHighlights();
} else {
setLogSummaryHighlights([]);
}
}, [
bucketSize,
debouncedLoadSummaryHighlights,
filterQuery,
highlightTerms,
sourceVersion,
startTimestamp,
endTimestamp,
]);
return {
logSummaryHighlights,
loadLogSummaryHighlightsRequest,
};
};

View file

@ -1,105 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isNumber } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { TimeKey, UniqueTimeKey } from '@kbn/io-ts-utils';
import {
getLogEntryIndexAtTime,
getLogEntryIndexBeforeTime,
getUniqueLogEntryKey,
} from '../../../utils/log_entry';
import { LogEntriesHighlightsResponse } from '../../../../common/http_api';
export const useNextAndPrevious = ({
highlightTerms,
jumpToTargetPosition,
logEntryHighlights,
visibleMidpoint,
}: {
highlightTerms: string[];
jumpToTargetPosition: (target: TimeKey) => void;
logEntryHighlights: LogEntriesHighlightsResponse['data'] | undefined;
visibleMidpoint: TimeKey | null;
}) => {
const [currentTimeKey, setCurrentTimeKey] = useState<UniqueTimeKey | null>(null);
const entries = useMemo(
// simplification, because we only support one highlight phrase for now
() =>
logEntryHighlights && logEntryHighlights.length > 0 ? logEntryHighlights[0].entries : [],
[logEntryHighlights]
);
useEffect(() => {
setCurrentTimeKey(null);
}, [highlightTerms]);
useEffect(() => {
if (currentTimeKey) {
jumpToTargetPosition(currentTimeKey);
}
}, [currentTimeKey, jumpToTargetPosition]);
useEffect(() => {
if (currentTimeKey === null && entries.length > 0) {
const initialIndex = visibleMidpoint
? clampValue(getLogEntryIndexBeforeTime(entries, visibleMidpoint), 0, entries.length - 1)
: 0;
const initialTimeKey = getUniqueLogEntryKey(entries[initialIndex]);
setCurrentTimeKey(initialTimeKey);
}
}, [currentTimeKey, entries, setCurrentTimeKey, visibleMidpoint]);
const indexOfCurrentTimeKey = useMemo(() => {
if (currentTimeKey && entries.length > 0) {
return getLogEntryIndexAtTime(entries, currentTimeKey);
} else {
return null;
}
}, [currentTimeKey, entries]);
const hasPreviousHighlight = useMemo(
() => isNumber(indexOfCurrentTimeKey) && indexOfCurrentTimeKey > 0,
[indexOfCurrentTimeKey]
);
const hasNextHighlight = useMemo(
() =>
entries.length > 0 &&
isNumber(indexOfCurrentTimeKey) &&
indexOfCurrentTimeKey < entries.length - 1,
[indexOfCurrentTimeKey, entries]
);
const goToPreviousHighlight = useCallback(() => {
if (entries.length && isNumber(indexOfCurrentTimeKey)) {
const previousIndex = indexOfCurrentTimeKey - 1;
const entryTimeKey = getUniqueLogEntryKey(entries[previousIndex]);
setCurrentTimeKey(entryTimeKey);
}
}, [indexOfCurrentTimeKey, entries]);
const goToNextHighlight = useCallback(() => {
if (entries.length > 0 && isNumber(indexOfCurrentTimeKey)) {
const nextIndex = indexOfCurrentTimeKey + 1;
const entryTimeKey = getUniqueLogEntryKey(entries[nextIndex]);
setCurrentTimeKey(entryTimeKey);
}
}, [indexOfCurrentTimeKey, entries]);
return {
currentHighlightKey: currentTimeKey,
hasPreviousHighlight,
hasNextHighlight,
goToPreviousHighlight,
goToNextHighlight,
};
};
const clampValue = (value: number, minValue: number, maxValue: number) =>
Math.min(Math.max(value, minValue), maxValue);

View file

@ -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 type { HttpHandler } from '@kbn/core/public';
import { decodeOrThrow } from '../../../../../common/runtime_types';
import {
LOG_ENTRIES_SUMMARY_PATH,
LogEntriesSummaryRequest,
logEntriesSummaryRequestRT,
logEntriesSummaryResponseRT,
} from '../../../../../common/http_api';
export const fetchLogSummary = async (
requestArgs: LogEntriesSummaryRequest,
fetch: HttpHandler
) => {
const response = await fetch(LOG_ENTRIES_SUMMARY_PATH, {
method: 'POST',
body: JSON.stringify(logEntriesSummaryRequestRT.encode(requestArgs)),
version: '1',
});
return decodeOrThrow(logEntriesSummaryResponseRT)(response);
};

View file

@ -1,24 +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';
const SUMMARY_BUCKET_COUNT = 100;
export function useBucketSize(
startTimestamp: number | null,
endTimestamp: number | null
): number | null {
const bucketSize = useMemo(() => {
if (!startTimestamp || !endTimestamp) {
return null;
}
return (endTimestamp - startTimestamp) / SUMMARY_BUCKET_COUNT;
}, [startTimestamp, endTimestamp]);
return bucketSize;
}

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './log_summary';
export * from './with_summary';

View file

@ -1,215 +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 { waitFor, renderHook } from '@testing-library/react';
// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock`
import { coreMock as mockCoreMock } from '@kbn/core/public/mocks';
import { useLogSummary } from './log_summary';
import { fetchLogSummary } from './api/fetch_log_summary';
import { datemathToEpochMillis } from '../../../utils/datemath';
const LOG_VIEW_REFERENCE = { type: 'log-view-reference' as const, logViewId: 'LOG_VIEW_ID' };
const CHANGED_LOG_VIEW_REFERENCE = {
type: 'log-view-reference' as const,
logViewId: 'CHANGED_LOG_VIEW_ID',
};
// Typescript doesn't know that `fetchLogSummary` is a jest mock.
// We use a second variable with a type cast to help the compiler further down the line.
jest.mock('./api/fetch_log_summary', () => ({ fetchLogSummary: jest.fn() }));
const fetchLogSummaryMock = fetchLogSummary as jest.MockedFunction<typeof fetchLogSummary>;
jest.mock('../../../hooks/use_kibana', () => {
const services = mockCoreMock.createStart();
return {
useKibanaContextForPlugin: () => ({ services }),
};
});
describe('useLogSummary hook', () => {
beforeEach(() => {
fetchLogSummaryMock.mockClear();
});
it('provides an empty list of buckets by default', () => {
const { result } = renderHook(() => useLogSummary(LOG_VIEW_REFERENCE, null, null, null));
expect(result.current.buckets).toEqual([]);
});
it('queries for new summary buckets when the source id changes', async () => {
const { startTimestamp, endTimestamp } = createMockDateRange();
const firstMockResponse = createMockResponse([
{ start: startTimestamp, end: endTimestamp, entriesCount: 1 },
]);
const secondMockResponse = createMockResponse([
{ start: startTimestamp, end: endTimestamp, entriesCount: 2 },
]);
fetchLogSummaryMock
.mockResolvedValueOnce(firstMockResponse)
.mockResolvedValueOnce(secondMockResponse);
const { result, rerender } = renderHook(
({ logViewReference }) => useLogSummary(logViewReference, startTimestamp, endTimestamp, null),
{
initialProps: { logViewReference: LOG_VIEW_REFERENCE },
}
);
await waitFor(() => new Promise((resolve) => resolve(null)));
expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1);
expect(fetchLogSummaryMock).toHaveBeenLastCalledWith(
expect.objectContaining({
logView: LOG_VIEW_REFERENCE,
}),
expect.anything()
);
expect(result.current.buckets).toEqual(firstMockResponse.data.buckets);
rerender({ logViewReference: CHANGED_LOG_VIEW_REFERENCE });
await waitFor(() => new Promise((resolve) => resolve(null)));
expect(fetchLogSummaryMock).toHaveBeenCalledTimes(2);
expect(fetchLogSummaryMock).toHaveBeenLastCalledWith(
expect.objectContaining({
logView: CHANGED_LOG_VIEW_REFERENCE,
}),
expect.anything()
);
expect(result.current.buckets).toEqual(secondMockResponse.data.buckets);
});
it('queries for new summary buckets when the filter query changes', async () => {
const { startTimestamp, endTimestamp } = createMockDateRange();
const firstMockResponse = createMockResponse([
{ start: startTimestamp, end: endTimestamp, entriesCount: 1 },
]);
const secondMockResponse = createMockResponse([
{ start: startTimestamp, end: endTimestamp, entriesCount: 2 },
]);
fetchLogSummaryMock
.mockResolvedValueOnce(firstMockResponse)
.mockResolvedValueOnce(secondMockResponse);
const { result, rerender } = renderHook(
({ filterQuery }) =>
useLogSummary(LOG_VIEW_REFERENCE, startTimestamp, endTimestamp, filterQuery),
{
initialProps: { filterQuery: 'INITIAL_FILTER_QUERY' },
}
);
await waitFor(() => new Promise((resolve) => resolve(null)));
expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1);
expect(fetchLogSummaryMock).toHaveBeenLastCalledWith(
expect.objectContaining({
query: 'INITIAL_FILTER_QUERY',
}),
expect.anything()
);
expect(result.current.buckets).toEqual(firstMockResponse.data.buckets);
rerender({ filterQuery: 'CHANGED_FILTER_QUERY' });
await waitFor(() => new Promise((resolve) => resolve(null)));
expect(fetchLogSummaryMock).toHaveBeenCalledTimes(2);
expect(fetchLogSummaryMock).toHaveBeenLastCalledWith(
expect.objectContaining({
query: 'CHANGED_FILTER_QUERY',
}),
expect.anything()
);
expect(result.current.buckets).toEqual(secondMockResponse.data.buckets);
});
it('queries for new summary buckets when the start and end date changes', async () => {
fetchLogSummaryMock
.mockResolvedValueOnce(createMockResponse([]))
.mockResolvedValueOnce(createMockResponse([]));
const firstRange = createMockDateRange();
const { rerender } = renderHook(
({ startTimestamp, endTimestamp }) =>
useLogSummary(LOG_VIEW_REFERENCE, startTimestamp, endTimestamp, null),
{
initialProps: firstRange,
}
);
await waitFor(() => new Promise((resolve) => resolve(null)));
expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1);
expect(fetchLogSummaryMock).toHaveBeenLastCalledWith(
expect.objectContaining({
startTimestamp: firstRange.startTimestamp,
endTimestamp: firstRange.endTimestamp,
}),
expect.anything()
);
const secondRange = createMockDateRange('now-20s', 'now');
rerender(secondRange);
await waitFor(() => new Promise((resolve) => resolve(null)));
expect(fetchLogSummaryMock).toHaveBeenCalledTimes(2);
expect(fetchLogSummaryMock).toHaveBeenLastCalledWith(
expect.objectContaining({
startTimestamp: secondRange.startTimestamp,
endTimestamp: secondRange.endTimestamp,
}),
expect.anything()
);
});
it("doesn't query for new summary buckets when the previous request is still in flight", async () => {
fetchLogSummaryMock.mockResolvedValueOnce(createMockResponse([]));
const firstRange = createMockDateRange();
const { rerender } = renderHook(
({ startTimestamp, endTimestamp }) =>
useLogSummary(LOG_VIEW_REFERENCE, startTimestamp, endTimestamp, null),
{
initialProps: firstRange,
}
);
const secondRange = createMockDateRange('now-20s', 'now');
// intentionally don't wait for an update to test the throttling
rerender(secondRange);
await waitFor(() => new Promise((resolve) => resolve(null)));
expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1);
expect(fetchLogSummaryMock).toHaveBeenLastCalledWith(
expect.objectContaining({
startTimestamp: firstRange.startTimestamp,
endTimestamp: firstRange.endTimestamp,
}),
expect.anything()
);
});
});
const createMockResponse = (
buckets: Array<{ start: number; end: number; entriesCount: number }>
) => ({ data: { buckets, start: Number.NEGATIVE_INFINITY, end: Number.POSITIVE_INFINITY } });
const createMockDateRange = (startDate = 'now-10s', endDate = 'now') => {
return {
startDate,
endDate,
startTimestamp: datemathToEpochMillis(startDate)!,
endTimestamp: datemathToEpochMillis(endDate, 'up')!,
};
};

View file

@ -1,74 +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 { exhaustMap, map, Observable } from 'rxjs';
import { HttpHandler } from '@kbn/core-http-browser';
import { LogViewReference } from '../../../../common';
import { useObservableState, useReplaySubject } from '../../../utils/use_observable';
import { fetchLogSummary } from './api/fetch_log_summary';
import { LogEntriesSummaryRequest, LogEntriesSummaryResponse } from '../../../../common/http_api';
import { useBucketSize } from './bucket_size';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
export type LogSummaryBuckets = LogEntriesSummaryResponse['data']['buckets'];
export const useLogSummary = (
logViewReference: LogViewReference,
startTimestamp: number | null,
endTimestamp: number | null,
filterQuery: string | null
) => {
const { services } = useKibanaContextForPlugin();
const bucketSize = useBucketSize(startTimestamp, endTimestamp);
const [logSummaryBuckets$, pushLogSummaryBucketsArgs] = useReplaySubject(fetchLogSummary$);
const { latestValue: logSummaryBuckets } = useObservableState(logSummaryBuckets$, NO_BUCKETS);
useEffect(() => {
if (startTimestamp === null || endTimestamp === null || bucketSize === null) {
return;
}
pushLogSummaryBucketsArgs([
{
logView: logViewReference,
startTimestamp,
endTimestamp,
bucketSize,
query: filterQuery,
},
services.http.fetch,
]);
}, [
bucketSize,
endTimestamp,
filterQuery,
pushLogSummaryBucketsArgs,
services.http.fetch,
logViewReference,
startTimestamp,
]);
return {
buckets: logSummaryBuckets,
start: startTimestamp,
end: endTimestamp,
};
};
const NO_BUCKETS: LogSummaryBuckets = [];
type FetchLogSummaryArgs = [args: LogEntriesSummaryRequest, fetch: HttpHandler];
const fetchLogSummary$ = (
fetchArguments$: Observable<FetchLogSummaryArgs>
): Observable<LogSummaryBuckets> =>
fetchArguments$.pipe(
exhaustMap(([args, fetch]) => fetchLogSummary(args, fetch)),
map(({ data: { buckets } }) => buckets)
);

View file

@ -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 useThrottle from 'react-use/lib/useThrottle';
import { useLogPositionStateContext, useLogViewContext } from '../../..';
import { RendererFunction } from '../../../utils/typed_react';
import { LogSummaryBuckets, useLogSummary } from './log_summary';
const FETCH_THROTTLE_INTERVAL = 3000;
export interface WithSummaryProps {
serializedParsedQuery: string | null;
children: RendererFunction<{
buckets: LogSummaryBuckets;
start: number | null;
end: number | null;
}>;
}
export const WithSummary = ({ serializedParsedQuery, children }: WithSummaryProps) => {
const { logViewReference } = useLogViewContext();
const { startTimestamp, endTimestamp } = useLogPositionStateContext();
// Keep it reasonably updated for the `now` case, but don't reload all the time when the user scrolls
const throttledStartTimestamp = useThrottle(startTimestamp, FETCH_THROTTLE_INTERVAL);
const throttledEndTimestamp = useThrottle(endTimestamp, FETCH_THROTTLE_INTERVAL);
const { buckets, start, end } = useLogSummary(
logViewReference,
throttledStartTimestamp,
throttledEndTimestamp,
serializedParsedQuery
);
return children({ buckets, start, end });
};
// eslint-disable-next-line import/no-default-export
export default WithSummary;

View file

@ -28,11 +28,6 @@ export {
LogPositionStateProvider,
useLogPositionStateContext,
} from './containers/logs/log_position';
export {
LogHighlightsStateProvider,
useLogHighlightsStateContext,
} from './containers/logs/log_highlights';
export type { LogSummaryBuckets, WithSummaryProps } from './containers/logs/log_summary';
// Shared components
export type { LogAIAssistantDocument } from './components/log_ai_assistant/log_ai_assistant';
@ -52,7 +47,6 @@ export type {
} from './components/logging/log_text_stream/scrollable_log_text_stream_view';
export type { LogsOverviewProps } from './components/logs_overview';
export const WithSummary = dynamic(() => import('./containers/logs/log_summary/with_summary'));
export const LogEntryFlyout = dynamic(
() => import('./components/logging/log_entry_flyout/log_entry_flyout')
);

View file

@ -5,11 +5,7 @@
* 2.0.
*/
import type { TimeKey, UniqueTimeKey } from '@kbn/io-ts-utils';
import { bisector } from 'd3-array';
import { compareToTimeKey, getIndexAtTimeKey } from '../../../common/time';
import {
LogEntry,
LogColumn,
LogTimestampColumn,
LogFieldColumn,
@ -19,25 +15,6 @@ import {
LogMessageConstantPart,
} from '../../../common/log_entry';
export const getLogEntryKey = (entry: { cursor: TimeKey }) => entry.cursor;
export const getUniqueLogEntryKey = (entry: { id: string; cursor: TimeKey }): UniqueTimeKey => ({
...entry.cursor,
gid: entry.id,
});
const logEntryTimeBisector = bisector(compareToTimeKey(getLogEntryKey));
export const getLogEntryIndexBeforeTime = logEntryTimeBisector.left;
export const getLogEntryIndexAfterTime = logEntryTimeBisector.right;
export const getLogEntryIndexAtTime = getIndexAtTimeKey(getLogEntryKey);
export const getLogEntryAtTime = (entries: LogEntry[], time: TimeKey) => {
const entryIndex = getLogEntryIndexAtTime(entries, time);
return entryIndex !== null ? entries[entryIndex] : null;
};
export const isTimestampColumn = (column: LogColumn): column is LogTimestampColumn =>
column != null && 'time' in column;

View file

@ -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.
*/
import React from 'react';
export type RendererResult = React.ReactElement<any> | null;
export type RendererFunction<RenderArgs, Result = RendererResult> = (args: RenderArgs) => Result;

View file

@ -6,14 +6,7 @@
*/
import { useEffect, useRef, useState } from 'react';
import {
BehaviorSubject,
Observable,
OperatorFunction,
PartialObserver,
ReplaySubject,
Subscription,
} from 'rxjs';
import { BehaviorSubject, Observable, OperatorFunction, PartialObserver, Subscription } from 'rxjs';
import { switchMap } from 'rxjs';
export const useLatest = <Value>(value: Value) => {
@ -58,22 +51,6 @@ export const useBehaviorSubject = <
return [output$, next] as const;
};
export const useReplaySubject = <
InputValue,
OutputValue,
OutputObservable extends Observable<OutputValue>
>(
deriveObservableOnce: (input$: Observable<InputValue>) => OutputObservable
) => {
const [[subject$, next], _] = useState(() => {
const newSubject$ = new ReplaySubject<InputValue>();
const newNext = newSubject$.next.bind(newSubject$);
return [newSubject$, newNext] as const;
});
const [output$] = useState(() => deriveObservableOnce(subject$));
return [output$, next] as const;
};
export const useObservableState = <State, InitialState>(
state$: Observable<State>,
initialState: InitialState | (() => InitialState)

View file

@ -5,11 +5,6 @@
* 2.0.
*/
import { timeMilliseconds } from 'd3-time';
import { fold, map } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
import * as runtimeTypes from 'io-ts';
import { JsonArray } from '@kbn/utility-types';
import { compact } from 'lodash';
import type { LogsSharedPluginRequestHandlerContext } from '../../../types';
@ -18,7 +13,6 @@ import {
LogEntriesParams,
LogEntryDocument,
LogEntryQuery,
LogSummaryBucket,
LOG_ENTRIES_PAGE_SIZE,
} from '../../domains/log_entries_domain';
import { SortedSearchHit } from '../framework';
@ -28,21 +22,6 @@ import { TIMESTAMP_FIELD, TIEBREAKER_FIELD } from '../../../../common/constants'
const TIMESTAMP_FORMAT = 'epoch_millis';
const MAX_BUCKETS = 1000;
function getBucketIntervalStarts(
startTimestamp: number,
endTimestamp: number,
bucketSize: number
): Date[] {
// estimated number of buckets
const bucketCount = Math.ceil((endTimestamp - startTimestamp) / bucketSize);
if (bucketCount > MAX_BUCKETS) {
throw new Error(`Requested too many buckets: ${bucketCount} > ${MAX_BUCKETS}`);
}
return timeMilliseconds(new Date(startTimestamp), new Date(endTimestamp), bucketSize);
}
export class LogsSharedKibanaLogEntriesAdapter implements LogEntriesAdapter {
constructor(private readonly framework: KibanaFramework) {}
@ -140,86 +119,6 @@ export class LogsSharedKibanaLogEntriesAdapter implements LogEntriesAdapter {
hasMoreAfter: sortDirection === 'asc' ? hasMore : undefined,
};
}
public async getContainedLogSummaryBuckets(
requestContext: LogsSharedPluginRequestHandlerContext,
resolvedLogView: ResolvedLogView,
startTimestamp: number,
endTimestamp: number,
bucketSize: number,
filterQuery?: LogEntryQuery
): Promise<LogSummaryBucket[]> {
const bucketIntervalStarts = getBucketIntervalStarts(startTimestamp, endTimestamp, bucketSize);
const query = {
allow_no_indices: true,
index: resolvedLogView.indices,
ignore_unavailable: true,
body: {
aggregations: {
count_by_date: {
date_range: {
field: TIMESTAMP_FIELD,
format: TIMESTAMP_FORMAT,
ranges: bucketIntervalStarts.map((bucketIntervalStart) => ({
from: bucketIntervalStart.getTime(),
to: bucketIntervalStart.getTime() + bucketSize,
})),
},
aggregations: {
top_hits_by_key: {
top_hits: {
size: 1,
sort: [
{
[TIMESTAMP_FIELD]: {
order: 'asc',
format: 'strict_date_optional_time_nanos',
numeric_type: 'date_nanos',
},
},
{ [TIEBREAKER_FIELD]: 'asc' },
],
_source: false,
},
},
},
},
},
query: {
bool: {
filter: [
...createQueryFilterClauses(filterQuery),
{
range: {
[TIMESTAMP_FIELD]: {
gte: startTimestamp,
lte: endTimestamp,
format: TIMESTAMP_FORMAT,
},
},
},
],
},
},
runtime_mappings: resolvedLogView.runtimeMappings,
size: 0,
track_total_hits: false,
},
};
const response = await this.framework.callWithRequest<any, {}>(requestContext, 'search', query);
return pipe(
LogSummaryResponseRuntimeType.decode(response),
map((logSummaryResponse) =>
logSummaryResponse.aggregations.count_by_date.buckets.map(
convertDateRangeBucketToSummaryBucket
)
),
fold(constant([]), identity)
);
}
}
function mapHitsToLogEntryDocuments(hits: SortedSearchHit[], fields: string[]): LogEntryDocument[] {
@ -245,18 +144,6 @@ function mapHitsToLogEntryDocuments(hits: SortedSearchHit[], fields: string[]):
});
}
const convertDateRangeBucketToSummaryBucket = (
bucket: LogSummaryDateRangeBucket
): LogSummaryBucket => ({
entriesCount: bucket.doc_count,
start: bucket.from || 0,
end: bucket.to || 0,
topEntryKeys: bucket.top_hits_by_key.hits.hits.map((hit) => ({
tiebreaker: hit.sort[1],
time: hit.sort[0],
})),
});
const createHighlightQuery = (
highlightTerm: string | undefined,
fields: string[]
@ -284,9 +171,6 @@ const createFilterClauses = (
return compact([filterQuery, highlightQuery]) as LogEntryQuery[];
};
const createQueryFilterClauses = (filterQuery: LogEntryQuery | undefined) =>
filterQuery ? [filterQuery] : [];
function processCursor(cursor: LogEntriesParams['cursor']): {
sortDirection: 'asc' | 'desc';
searchAfterClause: { search_after?: readonly [string, number] };
@ -310,35 +194,3 @@ function processCursor(cursor: LogEntriesParams['cursor']): {
return { sortDirection: 'asc', searchAfterClause: {} };
}
const LogSummaryDateRangeBucketRuntimeType = runtimeTypes.intersection([
runtimeTypes.type({
doc_count: runtimeTypes.number,
key: runtimeTypes.string,
top_hits_by_key: runtimeTypes.type({
hits: runtimeTypes.type({
hits: runtimeTypes.array(
runtimeTypes.type({
sort: runtimeTypes.tuple([runtimeTypes.string, runtimeTypes.number]),
})
),
}),
}),
}),
runtimeTypes.partial({
from: runtimeTypes.number,
to: runtimeTypes.number,
}),
]);
export type LogSummaryDateRangeBucket = runtimeTypes.TypeOf<
typeof LogSummaryDateRangeBucketRuntimeType
>;
const LogSummaryResponseRuntimeType = runtimeTypes.type({
aggregations: runtimeTypes.type({
count_by_date: runtimeTypes.type({
buckets: runtimeTypes.array(LogSummaryDateRangeBucketRuntimeType),
}),
}),
});

View file

@ -10,10 +10,7 @@ import { ILogsSharedLogEntriesDomain } from './log_entries_domain';
export const createLogsSharedLogEntriesDomainMock =
(): jest.Mocked<ILogsSharedLogEntriesDomain> => {
return {
getLogEntriesAround: jest.fn(),
getLogEntries: jest.fn(),
getLogSummaryBucketsBetween: jest.fn(),
getLogSummaryHighlightBucketsBetween: jest.fn(),
getLogEntryDatasets: jest.fn(),
};
};

View file

@ -7,11 +7,6 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { JsonObject } from '@kbn/utility-types';
import { subtractMillisecondsFromDate } from '../../../../common/utils';
import {
LogEntriesSummaryBucket,
LogEntriesSummaryHighlightsBucket,
} from '../../../../common/http_api';
import { LogColumn, LogEntry, LogEntryCursor } from '../../../../common/log_entry';
import {
LogViewColumnConfiguration,
@ -45,15 +40,6 @@ export interface LogEntriesParams {
highlightTerm?: string;
}
export interface LogEntriesAroundParams {
startTimestamp: number;
endTimestamp: number;
size?: number;
center: LogEntryCursor;
query?: JsonObject;
highlightTerm?: string;
}
export const LOG_ENTRIES_PAGE_SIZE = 200;
const FIELDS_FROM_CONTEXT = ['log.file.path', 'host.name', 'container.id'] as const;
@ -61,35 +47,12 @@ const FIELDS_FROM_CONTEXT = ['log.file.path', 'host.name', 'container.id'] as co
const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000;
export interface ILogsSharedLogEntriesDomain {
getLogEntriesAround(
requestContext: LogsSharedPluginRequestHandlerContext,
logView: LogViewReference,
params: LogEntriesAroundParams,
columnOverrides?: LogViewColumnConfiguration[]
): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }>;
getLogEntries(
requestContext: LogsSharedPluginRequestHandlerContext,
logView: LogViewReference,
params: LogEntriesParams,
columnOverrides?: LogViewColumnConfiguration[]
): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }>;
getLogSummaryBucketsBetween(
requestContext: LogsSharedPluginRequestHandlerContext,
logView: LogViewReference,
start: number,
end: number,
bucketSize: number,
filterQuery?: LogEntryQuery
): Promise<LogEntriesSummaryBucket[]>;
getLogSummaryHighlightBucketsBetween(
requestContext: LogsSharedPluginRequestHandlerContext,
logView: LogViewReference,
startTimestamp: number,
endTimestamp: number,
bucketSize: number,
highlightQueries: string[],
filterQuery?: LogEntryQuery
): Promise<LogEntriesSummaryHighlightsBucket[][]>;
getLogEntryDatasets(
requestContext: LogsSharedPluginRequestHandlerContext,
timestampField: string,
@ -106,66 +69,6 @@ export class LogsSharedLogEntriesDomain implements ILogsSharedLogEntriesDomain {
private readonly libs: Pick<LogsSharedBackendLibs, 'framework' | 'getStartServices'>
) {}
public async getLogEntriesAround(
requestContext: LogsSharedPluginRequestHandlerContext,
logView: LogViewReference,
params: LogEntriesAroundParams,
columnOverrides?: LogViewColumnConfiguration[]
): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> {
const { startTimestamp, endTimestamp, center, query, size, highlightTerm } = params;
/*
* For odd sizes we will round this value down for the first half, and up
* for the second. This keeps the center cursor right in the center.
*
* For even sizes the half before is one entry bigger than the half after.
* [1, 2, 3, 4, 5, *6*, 7, 8, 9, 10]
* | 5 entries | |4 entries|
*/
const halfSize = (size || LOG_ENTRIES_PAGE_SIZE) / 2;
const { entries: entriesBefore, hasMoreBefore } = await this.getLogEntries(
requestContext,
logView,
{
startTimestamp,
endTimestamp,
query,
cursor: { before: center },
size: Math.floor(halfSize),
highlightTerm,
},
columnOverrides
);
/*
* Elasticsearch's `search_after` returns documents after the specified cursor.
* - If we have documents before the center, we search after the last of
* those. The first document of the new group is the center.
* - If there were no documents, we search one milisecond before the
* center. It then becomes the first document.
*/
const cursorAfter =
entriesBefore.length > 0
? entriesBefore[entriesBefore.length - 1].cursor
: { time: subtractMillisecondsFromDate(center.time, 1), tiebreaker: 0 };
const { entries: entriesAfter, hasMoreAfter } = await this.getLogEntries(
requestContext,
logView,
{
startTimestamp,
endTimestamp,
query,
cursor: { after: cursorAfter },
size: Math.ceil(halfSize),
highlightTerm,
}
);
return { entries: [...entriesBefore, ...entriesAfter], hasMoreBefore, hasMoreAfter };
}
public async getLogEntries(
requestContext: LogsSharedPluginRequestHandlerContext,
logView: LogViewReference,
@ -227,86 +130,6 @@ export class LogsSharedLogEntriesDomain implements ILogsSharedLogEntriesDomain {
return { entries, hasMoreBefore, hasMoreAfter };
}
public async getLogSummaryBucketsBetween(
requestContext: LogsSharedPluginRequestHandlerContext,
logView: LogViewReference,
start: number,
end: number,
bucketSize: number,
filterQuery?: LogEntryQuery
): Promise<LogEntriesSummaryBucket[]> {
const [, { logsDataAccess }, { logViews }] = await this.libs.getStartServices();
const { savedObjects, elasticsearch } = await requestContext.core;
const logSourcesService = logsDataAccess.services.logSourcesServiceFactory.getLogSourcesService(
savedObjects.client
);
const resolvedLogView = await logViews
.getClient(savedObjects.client, elasticsearch.client.asCurrentUser, logSourcesService)
.getResolvedLogView(logView);
const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets(
requestContext,
resolvedLogView,
start,
end,
bucketSize,
filterQuery
);
return dateRangeBuckets;
}
public async getLogSummaryHighlightBucketsBetween(
requestContext: LogsSharedPluginRequestHandlerContext,
logView: LogViewReference,
startTimestamp: number,
endTimestamp: number,
bucketSize: number,
highlightQueries: string[],
filterQuery?: LogEntryQuery
): Promise<LogEntriesSummaryHighlightsBucket[][]> {
const [, { logsDataAccess }, { logViews }] = await this.libs.getStartServices();
const { savedObjects, elasticsearch } = await requestContext.core;
const logSourcesService = logsDataAccess.services.logSourcesServiceFactory.getLogSourcesService(
savedObjects.client
);
const resolvedLogView = await logViews
.getClient(savedObjects.client, elasticsearch.client.asCurrentUser, logSourcesService)
.getResolvedLogView(logView);
const messageFormattingRules = compileFormattingRules(
getBuiltinRules(resolvedLogView.messageField)
);
const requiredFields = getRequiredFields(resolvedLogView, messageFormattingRules);
const summaries = await Promise.all(
highlightQueries.map(async (highlightQueryPhrase) => {
const highlightQuery = createHighlightQueryDsl(highlightQueryPhrase, requiredFields);
const query = filterQuery
? {
bool: {
must: [filterQuery, highlightQuery],
},
}
: highlightQuery;
const summaryBuckets = await this.adapter.getContainedLogSummaryBuckets(
requestContext,
resolvedLogView,
startTimestamp,
endTimestamp,
bucketSize,
query
);
const summaryHighlightBuckets = summaryBuckets
.filter(logSummaryBucketHasEntries)
.map(convertLogSummaryBucketToSummaryHighlightBucket);
return summaryHighlightBuckets;
})
);
return summaries;
}
public async getLogEntryDatasets(
requestContext: LogsSharedPluginRequestHandlerContext,
timestampField: string,
@ -356,15 +179,6 @@ export interface LogEntriesAdapter {
fields: string[],
params: LogEntriesParams
): Promise<{ documents: LogEntryDocument[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }>;
getContainedLogSummaryBuckets(
requestContext: LogsSharedPluginRequestHandlerContext,
resolvedLogView: ResolvedLogView,
startTimestamp: number,
endTimestamp: number,
bucketSize: number,
filterQuery?: LogEntryQuery
): Promise<LogSummaryBucket[]>;
}
export type LogEntryQuery = JsonObject;
@ -377,25 +191,6 @@ export interface LogEntryDocument {
cursor: LogEntryCursor;
}
export interface LogSummaryBucket {
entriesCount: number;
start: number;
end: number;
topEntryKeys: LogEntryCursor[];
}
const logSummaryBucketHasEntries = (bucket: LogSummaryBucket) =>
bucket.entriesCount > 0 && bucket.topEntryKeys.length > 0;
const convertLogSummaryBucketToSummaryHighlightBucket = (
bucket: LogSummaryBucket
): LogEntriesSummaryHighlightsBucket => ({
entriesCount: bucket.entriesCount,
start: bucket.start,
end: bucket.end,
representativeKey: bucket.topEntryKeys[0],
});
const getRequiredFields = (
configuration: ResolvedLogView,
messageFormattingRules: CompiledLogMessageFormattingRule
@ -416,15 +211,6 @@ const getRequiredFields = (
);
};
const createHighlightQueryDsl = (phrase: string, fields: string[]) => ({
multi_match: {
fields,
lenient: true,
query: phrase,
type: 'phrase',
},
});
const getContextFromDoc = (doc: LogEntryDocument): LogEntry['context'] => {
// Get all context fields, then test for the presence and type of the ones that go together
const containerId = doc.fields['container.id']?.[0];

View file

@ -6,18 +6,11 @@
*/
import { LogsSharedBackendLibs } from './lib/logs_shared_types';
import {
initLogEntriesHighlightsRoute,
initLogEntriesSummaryHighlightsRoute,
initLogEntriesSummaryRoute,
} from './routes/log_entries';
import { initLogViewRoutes } from './routes/log_views';
import { initMigrateLogViewSettingsRoute } from './routes/deprecations';
export const initLogsSharedServer = (libs: LogsSharedBackendLibs) => {
initLogEntriesHighlightsRoute(libs);
initLogEntriesSummaryRoute(libs);
initLogEntriesSummaryHighlightsRoute(libs);
initLogViewRoutes(libs);
initMigrateLogViewSettingsRoute(libs);
};

View file

@ -1,118 +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 Boom from '@hapi/boom';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { logEntriesV1 } from '../../../common/http_api';
import { throwErrors } from '../../../common/runtime_types';
import { LogsSharedBackendLibs } from '../../lib/logs_shared_types';
import { parseFilterQuery } from '../../utils/serialized_query';
import { LogEntriesParams } from '../../lib/domains/log_entries_domain';
const escapeHatch = schema.object({}, { unknowns: 'allow' });
export const initLogEntriesHighlightsRoute = ({ framework, logEntries }: LogsSharedBackendLibs) => {
framework
.registerVersionedRoute({
access: 'internal',
method: 'post',
path: logEntriesV1.LOG_ENTRIES_HIGHLIGHTS_PATH,
})
.addVersion(
{
version: '1',
validate: { request: { body: escapeHatch } },
options: {
deprecated: {
documentationUrl: '',
severity: 'warning',
message: i18n.translate(
'xpack.logsShared.deprecations.postLogEntriesHighlightsRoute.message',
{
defaultMessage:
'Given the deprecation of the LogStream feature, this API will not return reliable data in upcoming versions of Kibana.',
}
),
reason: { type: 'deprecate' },
},
},
},
async (requestContext, request, response) => {
const payload = pipe(
logEntriesV1.logEntriesHighlightsRequestRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const { startTimestamp, endTimestamp, logView, query, size, highlightTerms } = payload;
let entriesPerHighlightTerm;
if ('center' in payload) {
entriesPerHighlightTerm = await Promise.all(
highlightTerms.map((highlightTerm) =>
logEntries.getLogEntriesAround(requestContext, logView, {
startTimestamp,
endTimestamp,
query: parseFilterQuery(query),
center: payload.center,
size,
highlightTerm,
})
)
);
} else {
let cursor: LogEntriesParams['cursor'];
if ('before' in payload) {
cursor = { before: payload.before };
} else if ('after' in payload) {
cursor = { after: payload.after };
}
entriesPerHighlightTerm = await Promise.all(
highlightTerms.map((highlightTerm) =>
logEntries.getLogEntries(requestContext, logView, {
startTimestamp,
endTimestamp,
query: parseFilterQuery(query),
cursor,
size,
highlightTerm,
})
)
);
}
return response.ok({
body: logEntriesV1.logEntriesHighlightsResponseRT.encode({
data: entriesPerHighlightTerm.map(({ entries }) => {
if (entries.length > 0) {
return {
entries,
topCursor: entries[0].cursor,
bottomCursor: entries[entries.length - 1].cursor,
};
} else {
return {
entries,
topCursor: null,
bottomCursor: null,
};
}
}),
}),
});
}
);
};

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './highlights';
export * from './summary';
export * from './summary_highlights';

View file

@ -1,88 +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 Boom from '@hapi/boom';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { logEntriesV1 } from '../../../common/http_api';
import { throwErrors } from '../../../common/runtime_types';
import { LogsSharedBackendLibs } from '../../lib/logs_shared_types';
import { parseFilterQuery } from '../../utils/serialized_query';
const escapeHatch = schema.object({}, { unknowns: 'allow' });
export const initLogEntriesSummaryRoute = ({
framework,
logEntries,
getUsageCollector,
}: LogsSharedBackendLibs) => {
framework
.registerVersionedRoute({
access: 'internal',
method: 'post',
path: logEntriesV1.LOG_ENTRIES_SUMMARY_PATH,
})
.addVersion(
{
version: '1',
validate: { request: { body: escapeHatch } },
options: {
deprecated: {
documentationUrl: '',
severity: 'warning',
message: i18n.translate(
'xpack.logsShared.deprecations.postLogEntriesSummaryRoute.message',
{
defaultMessage:
'Given the deprecation of the LogStream feature, this API will not return reliable data in upcoming versions of Kibana.',
}
),
reason: { type: 'deprecate' },
},
},
},
async (requestContext, request, response) => {
const payload = pipe(
logEntriesV1.logEntriesSummaryRequestRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const { logView, startTimestamp, endTimestamp, bucketSize, query } = payload;
const usageCollector = getUsageCollector();
const buckets = await logEntries.getLogSummaryBucketsBetween(
requestContext,
logView,
startTimestamp,
endTimestamp,
bucketSize,
parseFilterQuery(query)
);
if (typeof usageCollector.countLogs === 'function') {
usageCollector.countLogs();
}
return response.ok({
body: logEntriesV1.logEntriesSummaryResponseRT.encode({
data: {
start: startTimestamp,
end: endTimestamp,
buckets,
},
}),
});
}
);
};

View file

@ -1,83 +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 Boom from '@hapi/boom';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { logEntriesV1 } from '../../../common/http_api';
import { throwErrors } from '../../../common/runtime_types';
import { LogsSharedBackendLibs } from '../../lib/logs_shared_types';
import { parseFilterQuery } from '../../utils/serialized_query';
const escapeHatch = schema.object({}, { unknowns: 'allow' });
export const initLogEntriesSummaryHighlightsRoute = ({
framework,
logEntries,
}: LogsSharedBackendLibs) => {
framework
.registerVersionedRoute({
access: 'internal',
method: 'post',
path: logEntriesV1.LOG_ENTRIES_SUMMARY_HIGHLIGHTS_PATH,
})
.addVersion(
{
version: '1',
validate: { request: { body: escapeHatch } },
options: {
deprecated: {
documentationUrl: '',
severity: 'warning',
message: i18n.translate(
'xpack.logsShared.deprecations.postLogEntriesSummaryHighlightsRoute.message',
{
defaultMessage:
'Given the deprecation of the LogStream feature, this API will not return reliable data in upcoming versions of Kibana.',
}
),
reason: { type: 'deprecate' },
},
},
},
async (requestContext, request, response) => {
const payload = pipe(
logEntriesV1.logEntriesSummaryHighlightsRequestRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const { logView, startTimestamp, endTimestamp, bucketSize, query, highlightTerms } =
payload;
const bucketsPerHighlightTerm = await logEntries.getLogSummaryHighlightBucketsBetween(
requestContext,
logView,
startTimestamp,
endTimestamp,
bucketSize,
highlightTerms,
parseFilterQuery(query)
);
return response.ok({
body: logEntriesV1.logEntriesSummaryHighlightsResponseRT.encode({
data: bucketsPerHighlightTerm.map((buckets) => ({
start: startTimestamp,
end: endTimestamp,
buckets,
})),
}),
});
}
);
};

View file

@ -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.
*/
export type { SearchResult } from './log_search_result';
export {
getSearchResultIndexBeforeTime,
getSearchResultIndexAfterTime,
getSearchResultKey,
} from './log_search_result';

View file

@ -1,32 +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 { bisector } from 'd3-array';
import type { TimeKey } from '../time';
import { compareToTimeKey } from '../time';
export interface SearchResult {
gid: string;
fields: TimeKey;
matches: SearchResultFieldMatches;
}
export interface SearchResultFieldMatches {
[field: string]: string[];
}
export const getSearchResultKey = (result: SearchResult) =>
({
gid: result.gid,
tiebreaker: result.fields.tiebreaker,
time: result.fields.time,
} as TimeKey);
const searchResultTimeBisector = bisector(compareToTimeKey(getSearchResultKey));
export const getSearchResultIndexBeforeTime = searchResultTimeBisector.left;
export const getSearchResultIndexAfterTime = searchResultTimeBisector.right;

View file

@ -1,8 +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 { SearchSummaryBucket } from './log_search_summary';

View file

@ -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 type { SearchResult } from '../log_search_result';
export interface SearchSummaryBucket {
start: number;
end: number;
count: number;
representative: SearchResult;
}

View file

@ -1,8 +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 './log_text_scale';

View file

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type TextScale = 'small' | 'medium' | 'large';
export function isTextScale(maybeTextScale: string): maybeTextScale is TextScale {
return ['small', 'medium', 'large'].includes(maybeTextScale);
}

View file

@ -6,7 +6,7 @@
*/
import { DateFromStringOrNumber } from '@kbn/io-ts-utils';
import { ascending, bisector } from 'd3-array';
import { ascending } from 'd3-array';
import * as rt from 'io-ts';
import { pick } from 'lodash';
@ -69,28 +69,6 @@ export const compareToTimeKey =
(value: Value, key: TimeKey) =>
compareTimeKeys(keyAccessor(value), key, compareValues);
export const getIndexAtTimeKey = <Value>(
keyAccessor: (value: Value) => TimeKey,
compareValues?: Comparator
) => {
const comparator = compareToTimeKey(keyAccessor, compareValues);
const collectionBisector = bisector(comparator);
return (collection: Value[], key: TimeKey): number | null => {
const index = collectionBisector.left(collection, key);
if (index >= collection.length) {
return null;
}
if (comparator(collection[index], key) !== 0) {
return null;
}
return index;
};
};
export const timeKeyIsBetween = (min: TimeKey, max: TimeKey, operand: TimeKey) =>
compareTimeKeys(min, operand) <= 0 && compareTimeKeys(max, operand) >= 0;

View file

@ -7,39 +7,12 @@
import { url } from '@kbn/kibana-utils-plugin/common';
import { encode } from '@kbn/rison';
import type { Query } from '@kbn/es-query';
import { parse, stringify } from 'query-string';
import type { DurationInputObject } from 'moment';
import moment from 'moment';
import type { LogViewReference } from '@kbn/logs-shared-plugin/common';
import {
defaultFilterStateKey,
defaultPositionStateKey,
DEFAULT_REFRESH_INTERVAL,
} from '@kbn/logs-shared-plugin/common';
import type { FilterStateInUrl } from '../public/observability_logs/log_stream_query_state';
import type { PositionStateInUrl } from '../public/observability_logs/log_stream_position_state/src/url_state_storage_service';
import type { TimeRange } from './time';
export const defaultLogViewKey = 'logView';
const encodeRisonUrlState = (state: any) => encode(state);
// Used by Locator components
export const replaceLogPositionInQueryString = (time?: number) =>
Number.isNaN(time) || time == null
? (value: string) => value
: replaceStateKeyInQueryString<PositionStateInUrl>(defaultPositionStateKey, {
position: {
time: moment(time).toISOString(),
tiebreaker: 0,
},
});
// NOTE: Used by Locator components
export const replaceLogViewInQueryString = (logViewReference: LogViewReference) =>
replaceStateKeyInQueryString(defaultLogViewKey, logViewReference);
export const replaceStateKeyInQueryString =
<UrlState extends any>(stateKey: string, urlState: UrlState | undefined) =>
(queryString: string) => {
@ -53,38 +26,3 @@ export const replaceStateKeyInQueryString =
};
return stringify(url.encodeQuery(newValue), { sort: false, encode: false });
};
export const replaceLogFilterInQueryString = (query: Query, time?: number, timeRange?: TimeRange) =>
replaceStateKeyInQueryString<FilterStateInUrl>(defaultFilterStateKey, {
query,
...getTimeRange(time, timeRange),
refreshInterval: DEFAULT_REFRESH_INTERVAL,
});
const getTimeRange = (time?: number, timeRange?: TimeRange) => {
if (timeRange) {
return {
timeRange: {
from: new Date(timeRange.startTime).toISOString(),
to: new Date(timeRange.endTime).toISOString(),
},
};
} else if (time) {
return {
timeRange: {
from: getTimeRangeStartFromTime(time),
to: getTimeRangeEndFromTime(time),
},
};
} else {
return {};
}
};
const defaultTimeRangeFromPositionOffset: DurationInputObject = { hours: 1 };
export const getTimeRangeStartFromTime = (time: number): string =>
moment(time).subtract(defaultTimeRangeFromPositionOffset).toISOString();
export const getTimeRangeEndFromTime = (time: number): string =>
moment(time).add(defaultTimeRangeFromPositionOffset).toISOString();

View file

@ -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 React from 'react';
import { EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public';
import { EuiEmptyPrompt } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { PageTemplate } from '../page_template';
type InlineLogViewSplashPageProps = {
revertToDefaultLogView: () => void;
} & LazyObservabilityPageTemplateProps;
export const InlineLogViewSplashPage: React.FC<InlineLogViewSplashPageProps> = (props) => {
const { revertToDefaultLogView, ...templateProps } = props;
return (
<PageTemplate {...templateProps} isEmptyState={true}>
<InlineLogViewSplashPrompt revertToDefaultLogView={revertToDefaultLogView} />
</PageTemplate>
);
};
export const InlineLogViewSplashPrompt: React.FC<{
revertToDefaultLogView: InlineLogViewSplashPageProps['revertToDefaultLogView'];
}> = ({ revertToDefaultLogView }) => {
const title = (
<FormattedMessage
id="xpack.infra.ml.splash.inlineLogView.title"
defaultMessage="Switch to a persisted Log View"
/>
);
const ctaButton = (
<EuiButton
data-test-subj="infraInlineLogViewSplashPromptRevertToDefaultPersistedLogViewButton"
fullWidth={false}
fill
onClick={revertToDefaultLogView}
>
<FormattedMessage
id="xpack.infra.ml.splash.inlineLogView.buttonText"
defaultMessage="Revert to default (persisted) Log View"
/>
</EuiButton>
);
const description = (
<FormattedMessage
id="xpack.infra.ml.splash.inlineLogView.description"
defaultMessage="This feature does not support inline Log Views"
/>
);
return (
<EuiEmptyPrompt
iconType={'visLine'}
title={<h2>{title}</h2>}
body={
<EuiText>
<p>{description}</p>
</EuiText>
}
actions={ctaButton}
/>
);
};

View file

@ -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 { EuiButtonEmpty, EuiPopover } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import * as React from 'react';
import styled from '@emotion/styled';
interface LogCustomizationMenuState {
isShown: boolean;
}
export class LogCustomizationMenu extends React.Component<
React.PropsWithChildren<{}>,
LogCustomizationMenuState
> {
public readonly state = {
isShown: false,
};
public show = () => {
this.setState({
isShown: true,
});
};
public hide = () => {
this.setState({
isShown: false,
});
};
public toggleVisibility = () => {
this.setState((state) => ({
isShown: !state.isShown,
}));
};
public render() {
const { children } = this.props;
const { isShown } = this.state;
const menuButton = (
<EuiButtonEmpty
data-test-subj="infraCustomizeButton"
color="text"
iconType="eye"
onClick={this.toggleVisibility}
size="xs"
>
<FormattedMessage
id="xpack.infra.logs.customizeLogs.customizeButtonLabel"
defaultMessage="Customize"
/>
</EuiButtonEmpty>
);
return (
<EuiPopover
id="customizePopover"
button={menuButton}
closePopover={this.hide}
isOpen={isShown}
anchorPosition="downRight"
ownFocus
>
<CustomizationMenuContent>{children}</CustomizationMenuContent>
</EuiPopover>
);
}
}
const CustomizationMenuContent = styled.div`
min-width: 200px;
`;

View file

@ -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, { useCallback } from 'react';
import type { OnTimeChangeProps } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
interface LogDatepickerProps {
startDateExpression: string;
endDateExpression: string;
isStreaming: boolean;
onUpdateDateRange?: (range: { startDateExpression: string; endDateExpression: string }) => void;
onStartStreaming?: () => void;
onStopStreaming?: () => void;
}
export const LogDatepicker: React.FC<LogDatepickerProps> = ({
startDateExpression,
endDateExpression,
isStreaming,
onUpdateDateRange,
onStartStreaming,
onStopStreaming,
}) => {
const handleTimeChange = useCallback(
({ start, end, isInvalid }: OnTimeChangeProps) => {
if (onUpdateDateRange && !isInvalid) {
onUpdateDateRange({ startDateExpression: start, endDateExpression: end });
}
},
[onUpdateDateRange]
);
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiSuperDatePicker
start={startDateExpression}
end={endDateExpression}
onTimeChange={handleTimeChange}
showUpdateButton={false}
// @ts-ignore: EuiSuperDatePicker doesn't expose the `isDisabled` prop, although it exists.
isDisabled={isStreaming}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{isStreaming ? (
<EuiButton
data-test-subj="infraLogDatepickerStopStreamingButton"
color="primary"
iconType="pause"
iconSide="left"
onClick={onStopStreaming}
>
<FormattedMessage
id="xpack.infra.logs.stopStreamingButtonLabel"
defaultMessage="Stop streaming"
/>
</EuiButton>
) : (
<EuiButton
data-test-subj="infraLogDatepickerStreamLiveButton"
color="primary"
iconType="play"
iconSide="left"
onClick={onStartStreaming}
>
<FormattedMessage
id="xpack.infra.logs.startStreamingButtonLabel"
defaultMessage="Stream live"
/>
</EuiButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,190 +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 {
EuiButtonEmpty,
EuiButtonIcon,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { debounce } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import styled from '@emotion/styled';
import { useVisibilityState } from '../../hooks/use_visibility_state';
import { withAttrs } from '../../utils/theme_utils/with_attrs';
interface LogHighlightsMenuProps {
onChange: (highlightTerms: string[]) => void;
isLoading: boolean;
activeHighlights: boolean;
hasPreviousHighlight: boolean;
hasNextHighlight: boolean;
goToPreviousHighlight: () => void;
goToNextHighlight: () => void;
}
export const LogHighlightsMenu: React.FC<LogHighlightsMenuProps> = ({
onChange,
isLoading,
activeHighlights,
hasPreviousHighlight,
goToPreviousHighlight,
hasNextHighlight,
goToNextHighlight,
}) => {
const {
isVisible: isPopoverOpen,
hide: closePopover,
toggle: togglePopover,
} = useVisibilityState(false);
// Input field state
const [highlightTerm, _setHighlightTerm] = useState('');
const debouncedOnChange = useMemo(() => debounce(onChange, 275), [onChange]);
const setHighlightTerm = useCallback<typeof _setHighlightTerm>(
(valueOrUpdater) =>
_setHighlightTerm((previousHighlightTerm) => {
const newHighlightTerm =
typeof valueOrUpdater === 'function'
? valueOrUpdater(previousHighlightTerm)
: valueOrUpdater;
if (newHighlightTerm !== previousHighlightTerm) {
debouncedOnChange([newHighlightTerm]);
}
return newHighlightTerm;
}),
[debouncedOnChange]
);
const changeHighlightTerm = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setHighlightTerm(value);
},
[setHighlightTerm]
);
const clearHighlightTerm = useCallback(() => setHighlightTerm(''), [setHighlightTerm]);
const button = (
<EuiButtonEmpty
data-test-subj="infraLogHighlightsMenuButton"
color="text"
size="xs"
iconType="brush"
onClick={togglePopover}
>
<FormattedMessage
id="xpack.infra.logs.highlights.highlightsPopoverButtonLabel"
defaultMessage="Highlights"
/>
{activeHighlights ? <ActiveHighlightsIndicator /> : null}
</EuiButtonEmpty>
);
return (
<EuiPopover
id="popover"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
ownFocus
>
<LogHighlightsMenuContent>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem>
<EuiFieldText
data-test-subj="infraLogHighlightsMenuFieldText"
placeholder={termsFieldLabel}
fullWidth={true}
value={highlightTerm}
onChange={changeHighlightTerm}
isLoading={isLoading}
aria-label={termsFieldLabel}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="infraLogHighlightsMenuButton"
aria-label={goToPreviousHighlightLabel}
iconType="arrowUp"
onClick={goToPreviousHighlight}
title={goToPreviousHighlightLabel}
isDisabled={!hasPreviousHighlight}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="infraLogHighlightsMenuButton"
aria-label={goToNextHighlightLabel}
iconType="arrowDown"
onClick={goToNextHighlight}
title={goToNextHighlightLabel}
isDisabled={!hasNextHighlight}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="infraLogHighlightsMenuButton"
aria-label={clearTermsButtonLabel}
color="danger"
isDisabled={highlightTerm === ''}
iconType="trash"
onClick={clearHighlightTerm}
title={clearTermsButtonLabel}
/>
</EuiFlexItem>
</EuiFlexGroup>
</LogHighlightsMenuContent>
</EuiPopover>
);
};
const termsFieldLabel = i18n.translate('xpack.infra.logs.highlights.highlightTermsFieldLabel', {
defaultMessage: 'Terms to highlight',
});
const clearTermsButtonLabel = i18n.translate(
'xpack.infra.logs.highlights.clearHighlightTermsButtonLabel',
{
defaultMessage: 'Clear terms to highlight',
}
);
const goToPreviousHighlightLabel = i18n.translate(
'xpack.infra.logs.highlights.goToPreviousHighlightButtonLabel',
{
defaultMessage: 'Jump to previous highlight',
}
);
const goToNextHighlightLabel = i18n.translate(
'xpack.infra.logs.highlights.goToNextHighlightButtonLabel',
{
defaultMessage: 'Jump to next highlight',
}
);
const ActiveHighlightsIndicator = withAttrs(
styled(EuiIcon)`
padding-left: ${(props) => props.theme.euiTheme.size.xs};
`,
({ theme }) => ({
type: 'checkInCircleFilled',
size: 'm',
color: theme?.euiTheme.colors.accent,
})
);
const LogHighlightsMenuContent = styled.div`
width: 300px;
`;

View file

@ -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 { scaleLinear, scaleTime } from 'd3-scale';
import { area, curveMonotoneY } from 'd3-shape';
import { max } from 'lodash';
import * as React from 'react';
import styled from '@emotion/styled';
import type { LogEntriesSummaryBucket } from '@kbn/logs-shared-plugin/common';
import { COLOR_MODES_STANDARD } from '@elastic/eui';
interface DensityChartProps {
buckets: LogEntriesSummaryBucket[];
end: number;
start: number;
width: number;
height: number;
}
export const DensityChart: React.FC<DensityChartProps> = ({
buckets,
start,
end,
width,
height,
}) => {
if (start >= end || height <= 0 || width <= 0 || buckets.length <= 0) {
return null;
}
const yScale = scaleTime().domain([start, end]).range([0, height]);
const xMax = max(buckets.map((bucket) => bucket.entriesCount)) || 0;
const xScale = scaleLinear().domain([0, xMax]).range([0, width]);
const path = area<LogEntriesSummaryBucket>()
.x0(xScale(0) ?? 0)
.x1((bucket) => xScale(bucket.entriesCount) ?? 0)
.y0((bucket) => yScale(bucket.start) ?? 0)
.y1((bucket) => yScale(bucket.end) ?? 0)
.curve(curveMonotoneY);
const firstBucket = buckets[0];
const lastBucket = buckets[buckets.length - 1];
const pathBuckets = [
// Make sure the graph starts at the count of the first point
{ start, end: start, entriesCount: firstBucket.entriesCount },
...buckets,
// Make sure the line ends at the height of the last point
{ start: lastBucket.end, end: lastBucket.end, entriesCount: lastBucket.entriesCount },
// If the last point is not at the end of the minimap, make sure it doesn't extend indefinitely and goes to 0
{ start: end, end, entriesCount: 0 },
];
const pathData = path(pathBuckets);
return (
<g>
<DensityChartPositiveBackground width={width} height={height} />
<PositiveAreaPath d={pathData || ''} />
</g>
);
};
const DensityChartPositiveBackground = styled.rect`
fill: ${(props) =>
props.theme.colorMode === COLOR_MODES_STANDARD.dark
? props.theme.euiTheme.colors.lightShade
: props.theme.euiTheme.colors.lightestShade};
`;
const PositiveAreaPath = styled.path`
fill: ${(props) =>
props.theme.colorMode === COLOR_MODES_STANDARD.dark
? props.theme.euiTheme.colors.mediumShade
: props.theme.euiTheme.colors.lightShade};
`;

View file

@ -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 * as React from 'react';
import styled from '@emotion/styled';
interface HighlightedIntervalProps {
className?: string;
getPositionOfTime: (time: number) => number;
start: number;
end: number;
targetWidth: number;
width: number;
target: number | null;
}
export const HighlightedInterval: React.FC<HighlightedIntervalProps> = ({
className,
end,
getPositionOfTime,
start,
targetWidth,
width,
target,
}) => {
const yStart = getPositionOfTime(start);
const yEnd = getPositionOfTime(end);
const yTarget = target && getPositionOfTime(target);
return (
<>
{yTarget && (
<HighlightTargetMarker
className={className}
x1={0}
x2={targetWidth}
y1={yTarget}
y2={yTarget}
/>
)}
<HighlightPolygon
className={className}
points={` ${targetWidth},${yStart} ${width},${yStart} ${width},${yEnd} ${targetWidth},${yEnd}`}
/>
</>
);
};
HighlightedInterval.displayName = 'HighlightedInterval';
const HighlightTargetMarker = styled.line`
stroke: ${(props) => props.theme.euiTheme.colors.primary};
stroke-width: 1;
`;
const HighlightPolygon = styled.polygon`
fill: ${(props) => props.theme.euiTheme.colors.primary};
fill-opacity: 0.3;
stroke: ${(props) => props.theme.euiTheme.colors.primary};
stroke-width: 1;
`;

View file

@ -1,8 +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 { LogMinimap } from './log_minimap';

View file

@ -1,184 +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 styled from '@emotion/styled';
import type {
LogEntriesSummaryBucket,
LogEntriesSummaryHighlightsBucket,
LogEntryTime,
} from '@kbn/logs-shared-plugin/common';
import { scaleLinear } from 'd3-scale';
import moment from 'moment';
import * as React from 'react';
import { COLOR_MODES_STANDARD } from '@elastic/eui';
import { DensityChart } from './density_chart';
import { HighlightedInterval } from './highlighted_interval';
import { SearchMarkers } from './search_markers';
import { TimeRuler } from './time_ruler';
interface Interval {
end: number;
start: number;
}
interface LogMinimapProps {
className?: string;
height: number;
highlightedInterval: Interval | null;
jumpToTarget: (params: LogEntryTime) => any;
summaryBuckets: LogEntriesSummaryBucket[];
summaryHighlightBuckets?: LogEntriesSummaryHighlightsBucket[];
target: number | null;
start: number | null;
end: number | null;
width: number;
}
interface LogMinimapState {
target: number | null;
timeCursorY: number;
}
// Wide enough to fit "September"
const TIMERULER_WIDTH = 50;
function calculateYScale(start: number | null, end: number | null, height: number) {
return scaleLinear()
.domain([start || 0, end || 0])
.range([0, height]);
}
export class LogMinimap extends React.Component<LogMinimapProps, LogMinimapState> {
constructor(props: LogMinimapProps) {
super(props);
this.state = {
timeCursorY: 0,
target: props.target,
};
}
public handleClick: React.MouseEventHandler<SVGSVGElement> = (event) => {
const minimapTop = event.currentTarget.getBoundingClientRect().top;
const clickedYPosition = event.clientY - minimapTop;
const clickedTime = Math.floor(this.getYScale().invert(clickedYPosition));
this.props.jumpToTarget({
tiebreaker: 0,
time: moment(clickedTime).toISOString(),
});
};
public getYScale = () => {
const { start, end, height } = this.props;
return calculateYScale(start, end, height);
};
public getPositionOfTime = (time: number) => {
return this.getYScale()(time) ?? 0;
};
private updateTimeCursor: React.MouseEventHandler<SVGSVGElement> = (event) => {
const svgPosition = event.currentTarget.getBoundingClientRect();
const timeCursorY = event.clientY - svgPosition.top;
this.setState({ timeCursorY });
};
public render() {
const {
start,
end,
className,
height,
highlightedInterval,
jumpToTarget,
summaryBuckets,
summaryHighlightBuckets,
width,
} = this.props;
const { timeCursorY, target } = this.state;
const [minTime, maxTime] = calculateYScale(start, end, height).domain();
const tickCount = height ? Math.floor(height / 50) : 12;
return (
<MinimapWrapper
className={className}
height={height}
preserveAspectRatio="none"
viewBox={`0 0 ${width} ${height}`}
width={width}
onClick={this.handleClick}
onMouseMove={this.updateTimeCursor}
>
<MinimapBorder x1={TIMERULER_WIDTH} x2={TIMERULER_WIDTH} y1={0} y2={height} />
<TimeRuler
start={minTime}
end={maxTime}
width={TIMERULER_WIDTH}
height={height}
tickCount={tickCount}
/>
<g transform={`translate(${TIMERULER_WIDTH}, 0)`}>
<DensityChart
buckets={summaryBuckets}
start={minTime}
end={maxTime}
width={width - TIMERULER_WIDTH}
height={height}
/>
<SearchMarkers
buckets={summaryHighlightBuckets || []}
start={minTime}
end={maxTime}
width={width - TIMERULER_WIDTH}
height={height}
jumpToTarget={jumpToTarget}
/>
</g>
{highlightedInterval ? (
<HighlightedInterval
end={moment(highlightedInterval.end).valueOf()}
getPositionOfTime={this.getPositionOfTime}
start={moment(highlightedInterval.start).valueOf()}
targetWidth={TIMERULER_WIDTH}
width={width}
target={target}
/>
) : null}
<TimeCursor x1={TIMERULER_WIDTH} x2={width} y1={timeCursorY} y2={timeCursorY} />
</MinimapWrapper>
);
}
}
const MinimapBorder = styled.line`
stroke: ${(props) => props.theme.euiTheme.colors.mediumShade};
stroke-width: 1px;
`;
const TimeCursor = styled.line`
pointer-events: none;
stroke-width: 1px;
stroke: ${(props) =>
props.theme.colorMode === COLOR_MODES_STANDARD.dark
? props.theme.euiTheme.colors.darkestShade
: props.theme.euiTheme.colors.darkShade};
`;
const MinimapWrapper = styled.svg`
cursor: pointer;
fill: ${(props) => props.theme.euiTheme.colors.emptyShade};
& ${TimeCursor} {
visibility: hidden;
}
&:hover ${TimeCursor} {
visibility: visible;
}
`;

View file

@ -1,125 +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 { FormattedMessage } from '@kbn/i18n-react';
import styled from '@emotion/styled';
import { keyframes } from '@emotion/react';
import type {
LogEntriesSummaryHighlightsBucket,
LogEntryTime,
} from '@kbn/logs-shared-plugin/common';
import * as React from 'react';
import { SearchMarkerTooltip } from './search_marker_tooltip';
interface SearchMarkerProps {
bucket: LogEntriesSummaryHighlightsBucket;
height: number;
width: number;
jumpToTarget: (target: LogEntryTime) => void;
}
interface SearchMarkerState {
hoveredPosition: ClientRect | null;
}
export class SearchMarker extends React.PureComponent<SearchMarkerProps, SearchMarkerState> {
public readonly state: SearchMarkerState = {
hoveredPosition: null,
};
public handleClick: React.MouseEventHandler<SVGGElement> = (evt) => {
evt.stopPropagation();
this.props.jumpToTarget(this.props.bucket.representativeKey);
};
public handleMouseEnter: React.MouseEventHandler<SVGGElement> = (evt) => {
this.setState({
hoveredPosition: evt.currentTarget.getBoundingClientRect(),
});
};
public handleMouseLeave: React.MouseEventHandler<SVGGElement> = () => {
this.setState({
hoveredPosition: null,
});
};
public render() {
const { bucket, height, width } = this.props;
const { hoveredPosition } = this.state;
const bulge =
bucket.entriesCount > 1 ? (
<SearchMarkerForegroundRect x="-2" y="-2" width="4" height={height + 2} rx="2" ry="2" />
) : (
<>
<SearchMarkerForegroundRect x="-1" y="0" width="2" height={height} />
<SearchMarkerForegroundRect
x="-2"
y={height / 2 - 2}
width="4"
height="4"
rx="2"
ry="2"
/>
</>
);
return (
<>
{hoveredPosition ? (
<SearchMarkerTooltip markerPosition={hoveredPosition}>
<FormattedMessage
id="xpack.infra.logs.searchResultTooltip"
defaultMessage="{bucketCount, plural, one {# highlighted entry} other {# highlighted entries}}"
values={{
bucketCount: bucket.entriesCount,
}}
/>
</SearchMarkerTooltip>
) : null}
<SearchMarkerGroup
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
<SearchMarkerBackgroundRect x="0" y="0" width={width} height={height} />
{bulge}
</SearchMarkerGroup>
</>
);
}
}
const fadeInAnimation = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`;
const SearchMarkerGroup = styled.g`
animation: ${fadeInAnimation} ${(props) => props.theme.euiTheme.animation.extraSlow} ease-in both;
`;
const SearchMarkerBackgroundRect = styled.rect`
fill: ${(props) => props.theme.euiTheme.colors.accent};
opacity: 0;
transition: opacity ${(props) => props.theme.euiTheme.animation.normal} ease-in;
cursor: pointer;
${SearchMarkerGroup}:hover & {
opacity: 0.3;
}
`;
const SearchMarkerForegroundRect = styled.rect`
fill: ${(props) => props.theme.euiTheme.colors.accent};
`;

View file

@ -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 type { WithEuiThemeProps } from '@elastic/eui';
import { calculatePopoverPosition, EuiPortal, withEuiTheme } from '@elastic/eui';
// @ts-expect-error style types not defined
import { euiToolTipStyles } from '@elastic/eui/lib/components/tool_tip/tool_tip.styles';
import { css } from '@emotion/react';
import * as React from 'react';
import { AutoSizer } from '../../auto_sizer';
const POPOVER_ARROW_SIZE = 12; // px, to position it later
interface SearchMarkerTooltipProps {
markerPosition: ClientRect;
children: React.ReactNode;
}
export class _SearchMarkerTooltip extends React.PureComponent<
SearchMarkerTooltipProps & WithEuiThemeProps
> {
public render() {
const { children, markerPosition, theme } = this.props;
const styles = euiToolTipStyles(theme);
return (
<EuiPortal>
<div>
<AutoSizer content={false} bounds>
{({ measureRef, bounds: { width, height } }) => {
const { top, left } =
width && height
? calculatePopoverPosition(markerPosition, { width, height }, 'left', 16, [
'left',
])
: {
left: -9999, // render off-screen before the first measurement
top: 0,
};
return (
<div
css={css([styles.euiToolTip, styles.left])}
style={{
left,
top,
}}
ref={measureRef}
>
<div
css={css([styles.euiToolTip__arrow, styles.arrowPositions.left])}
style={{ left: width || 0, top: (height || 0) / 2 - POPOVER_ARROW_SIZE / 2 }}
/>
<div>{children}</div>
</div>
);
}}
</AutoSizer>
</div>
</EuiPortal>
);
}
}
export const SearchMarkerTooltip = withEuiTheme<SearchMarkerTooltipProps>(_SearchMarkerTooltip);

View file

@ -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 type {
LogEntriesSummaryHighlightsBucket,
LogEntryTime,
} from '@kbn/logs-shared-plugin/common';
import classNames from 'classnames';
import { scaleTime } from 'd3-scale';
import * as React from 'react';
import { SearchMarker } from './search_marker';
interface SearchMarkersProps {
buckets: LogEntriesSummaryHighlightsBucket[];
className?: string;
end: number;
start: number;
width: number;
height: number;
jumpToTarget: (target: LogEntryTime) => void;
}
export class SearchMarkers extends React.PureComponent<SearchMarkersProps, {}> {
public render() {
const { buckets, start, end, width, height, jumpToTarget, className } = this.props;
const classes = classNames('minimapSearchMarkers', className);
if (start >= end || height <= 0 || Object.keys(buckets).length <= 0) {
return null;
}
const yScale = scaleTime().domain([start, end]).range([0, height]);
return (
<g transform={`translate(${width / 2}, 0)`} className={classes}>
{buckets.map((bucket) => (
<g
key={`${bucket.representativeKey.time}:${bucket.representativeKey.tiebreaker}`}
transform={`translate(0, ${yScale(bucket.start)})`}
>
<SearchMarker
bucket={bucket}
height={(yScale(bucket.end) ?? 0) - (yScale(bucket.start) ?? 0)}
width={width}
jumpToTarget={jumpToTarget}
/>
</g>
))}
</g>
);
}
}

View file

@ -1,24 +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.
*/
// The default d3-time-format is a bit strange for small ranges, so we will specify our own
export function getTimeLabelFormat(start: number, end: number): string | undefined {
const diff = Math.abs(end - start);
// 15 seconds
if (diff < 15 * 1000) {
return ':%S.%L';
}
// 16 minutes
if (diff < 16 * 60 * 1000) {
return '%I:%M:%S';
}
// Use D3's default
return;
}

View file

@ -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 { scaleTime } from 'd3-scale';
import * as React from 'react';
import styled from '@emotion/styled';
import { COLOR_MODES_STANDARD, useEuiFontSize } from '@elastic/eui';
import { useKibanaTimeZoneSetting } from '../../../hooks/use_kibana_time_zone_setting';
import { getTimeLabelFormat } from './time_label_formatter';
interface TimeRulerProps {
end: number;
height: number;
start: number;
tickCount: number;
width: number;
}
const useZonedDate = (timestamp: number) => {
const timeZone = useKibanaTimeZoneSetting();
const options = timeZone !== 'local' ? { timeZone } : undefined;
return new Date(new Date(timestamp).toLocaleString('en-US', options));
};
export const TimeRuler: React.FC<TimeRulerProps> = ({ end, height, start, tickCount, width }) => {
const startWithOffset = useZonedDate(start);
const endWithOffset = useZonedDate(end);
const yScale = scaleTime().domain([startWithOffset, endWithOffset]).range([0, height]);
const ticks = yScale.ticks(tickCount);
const formatTick = yScale.tickFormat(
tickCount,
getTimeLabelFormat(startWithOffset.getTime(), endWithOffset.getTime())
);
return (
<g>
{ticks.map((tick, tickIndex) => {
const y = yScale(tick) ?? 0;
return (
<g key={`tick${tickIndex}`}>
<TimeRulerTickLabel x={0} y={y - 4}>
{formatTick(tick)}
</TimeRulerTickLabel>
<TimeRulerGridLine x1={0} y1={y} x2={width} y2={y} />
</g>
);
})}
</g>
);
};
TimeRuler.displayName = 'TimeRuler';
const TimeRulerTickLabel = styled.text`
font-size: ${() => useEuiFontSize('xxxs').fontSize};
line-height: ${() => useEuiFontSize('s').lineHeight};
fill: ${(props) => props.theme.euiTheme.colors.textSubdued};
user-select: none;
pointer-events: none;
`;
const TimeRulerGridLine = styled.line`
stroke: ${(props) =>
props.theme.colorMode === COLOR_MODES_STANDARD.dark
? props.theme.euiTheme.colors.darkestShade
: props.theme.euiTheme.colors.darkShade};
stroke-opacity: 0.5;
stroke-width: 1px;
`;

View file

@ -1,8 +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 { LogSearchControls } from './log_search_controls';

View file

@ -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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { LogEntryTime } from '@kbn/logs-shared-plugin/common';
import classNames from 'classnames';
import * as React from 'react';
interface LogSearchButtonsProps {
className?: string;
jumpToTarget: (target: LogEntryTime) => void;
previousSearchResult: LogEntryTime | null;
nextSearchResult: LogEntryTime | null;
}
export class LogSearchButtons extends React.PureComponent<LogSearchButtonsProps, {}> {
public handleJumpToPreviousSearchResult: React.MouseEventHandler<HTMLButtonElement> = () => {
const { jumpToTarget, previousSearchResult } = this.props;
if (previousSearchResult) {
jumpToTarget(previousSearchResult);
}
};
public handleJumpToNextSearchResult: React.MouseEventHandler<HTMLButtonElement> = () => {
const { jumpToTarget, nextSearchResult } = this.props;
if (nextSearchResult) {
jumpToTarget(nextSearchResult);
}
};
public render() {
const { className, previousSearchResult, nextSearchResult } = this.props;
const classes = classNames('searchButtons', className);
const hasPreviousSearchResult = !!previousSearchResult;
const hasNextSearchResult = !!nextSearchResult;
return (
<EuiFlexGroup className={classes} gutterSize="xs">
<EuiFlexItem>
<EuiButtonEmpty
data-test-subj="infraPreviousButton"
onClick={this.handleJumpToPreviousSearchResult}
iconType="arrowLeft"
iconSide="left"
isDisabled={!hasPreviousSearchResult}
size="s"
>
<FormattedMessage
id="xpack.infra.logs.search.previousButtonLabel"
defaultMessage="Previous"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiButtonEmpty
data-test-subj="infraNextButton"
onClick={this.handleJumpToNextSearchResult}
iconType="arrowRight"
iconSide="right"
isDisabled={!hasNextSearchResult}
size="s"
>
<FormattedMessage id="xpack.infra.logs.search.nextButtonLabel" defaultMessage="Next" />
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
);
}
}

View file

@ -1,64 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import classNames from 'classnames';
import * as React from 'react';
import type { LogEntryTime } from '@kbn/logs-shared-plugin/common';
import { LogSearchButtons } from './log_search_buttons';
import { LogSearchInput } from './log_search_input';
interface LogSearchControlsProps {
className?: string;
clearSearch: () => any;
isLoadingSearchResults: boolean;
previousSearchResult: LogEntryTime | null;
nextSearchResult: LogEntryTime | null;
jumpToTarget: (target: LogEntryTime) => any;
search: (query: string) => any;
}
export class LogSearchControls extends React.PureComponent<LogSearchControlsProps, {}> {
public render() {
const {
className,
clearSearch,
isLoadingSearchResults,
previousSearchResult,
nextSearchResult,
jumpToTarget,
search,
} = this.props;
const classes = classNames('searchControls', className);
return (
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
justifyContent="flexStart"
className={classes}
>
<EuiFlexItem>
<LogSearchInput
isLoading={isLoadingSearchResults}
onClear={clearSearch}
onSearch={search}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogSearchButtons
previousSearchResult={previousSearchResult}
nextSearchResult={nextSearchResult}
jumpToTarget={jumpToTarget}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
}

View file

@ -1,85 +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 { EuiFieldSearch } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from '@emotion/styled';
import classNames from 'classnames';
import * as React from 'react';
interface LogSearchInputProps {
className?: string;
isLoading: boolean;
onSearch: (query: string) => void;
onClear: () => void;
}
interface LogSearchInputState {
query: string;
}
export const LogSearchInput = class extends React.PureComponent<
LogSearchInputProps,
LogSearchInputState
> {
public static displayName = 'LogSearchInput';
public readonly state = {
query: '',
};
public handleSubmit: React.FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
const { query } = this.state;
if (query === '') {
this.props.onClear();
} else {
this.props.onSearch(this.state.query);
}
};
public handleChangeQuery: React.ChangeEventHandler<HTMLInputElement> = (evt) => {
this.setState({
query: evt.target.value,
});
};
public render() {
const { className, isLoading } = this.props;
const { query } = this.state;
const classes = classNames('loggingSearchInput', className);
return (
<form onSubmit={this.handleSubmit}>
<PlainSearchField
aria-label={i18n.translate('xpack.infra.logs.search.searchInLogsAriaLabel', {
defaultMessage: 'search',
})}
className={classes}
fullWidth
isLoading={isLoading}
onChange={this.handleChangeQuery}
placeholder={i18n.translate('xpack.infra.logs.search.searchInLogsPlaceholder', {
defaultMessage: 'Search',
})}
value={query}
/>
</form>
);
}
};
const PlainSearchField = styled(EuiFieldSearch)`
background: transparent;
box-shadow: none;
&:focus {
box-shadow: inset 0 -2px 0 0 ${(props) => props.theme.euiTheme.colors.primary};
}
`;

View file

@ -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 { EuiFlexGroup, type EuiFlexGroupProps, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
import React from 'react';
export const LogStatusbar = (props: EuiFlexGroupProps) => {
const { euiTheme } = useEuiTheme();
return (
<EuiFlexGroup
alignItems="center"
gutterSize="none"
justifyContent="flexEnd"
css={css`
padding: ${euiTheme.size.s};
border-top: ${euiTheme.border.thin};
max-height: ${euiTheme.size.xxxl};
min-height: ${euiTheme.size.xxxl};
background-color: ${euiTheme.colors.emptyShade};
flex-direction: row;
`}
{...props}
/>
);
};
export const LogStatusbarItem = EuiFlexItem;

View file

@ -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 { EuiFormRow, EuiRadioGroup } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import * as React from 'react';
import type { TextScale } from '../../../common/log_text_scale';
import { isTextScale } from '../../../common/log_text_scale';
interface LogTextScaleControlsProps {
availableTextScales: TextScale[];
textScale: TextScale;
setTextScale: (scale: TextScale) => any;
}
export class LogTextScaleControls extends React.PureComponent<LogTextScaleControlsProps> {
public setTextScale = (textScale: string) => {
if (isTextScale(textScale)) {
this.props.setTextScale(textScale);
}
};
public render() {
const { availableTextScales, textScale } = this.props;
return (
<EuiFormRow
label={
<FormattedMessage
id="xpack.infra.logs.customizeLogs.textSizeFormRowLabel"
defaultMessage="Text Size"
/>
}
>
<EuiRadioGroup
data-test-subj="infraRadioGroup"
options={availableTextScales.map((availableTextScale: TextScale) => ({
id: availableTextScale.toString(),
label: (
<FormattedMessage
id="xpack.infra.logs.customizeLogs.textSizeRadioGroup"
defaultMessage="{textScale, select,
small {Small}
medium {Medium}
large {Large}
other {{textScale}}
}"
values={{
textScale: availableTextScale,
}}
/>
),
}))}
idSelected={textScale}
onChange={this.setTextScale}
/>
</EuiFormRow>
);
}
}

View file

@ -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 { EuiFormRow, EuiSwitch } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import * as React from 'react';
interface LogTextWrapControlsProps {
wrap: boolean;
setTextWrap: (scale: boolean) => any;
}
export class LogTextWrapControls extends React.PureComponent<LogTextWrapControlsProps> {
public toggleWrap = () => {
this.props.setTextWrap(!this.props.wrap);
};
public render() {
const { wrap } = this.props;
return (
<EuiFormRow
label={
<FormattedMessage
id="xpack.infra.logs.customizeLogs.lineWrappingFormRowLabel"
defaultMessage="Line Wrapping"
/>
}
>
<EuiSwitch
label={
<FormattedMessage
id="xpack.infra.logs.customizeLogs.wrapLongLinesSwitchLabel"
defaultMessage="Wrap long lines"
/>
}
checked={wrap}
onChange={this.toggleWrap}
/>
</EuiFormRow>
);
}
}

View file

@ -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.
*/
import { mountHook } from '@kbn/test-jest-helpers';
import { useLogViewConfiguration } from './log_view_configuration';
describe('useLogViewConfiguration hook', () => {
describe('textScale state', () => {
it('has a default value', () => {
const { getLastHookValue } = mountHook(() => useLogViewConfiguration().textScale);
expect(getLastHookValue()).toEqual('medium');
});
it('can be updated', () => {
const { act, getLastHookValue } = mountHook(() => useLogViewConfiguration());
act(({ setTextScale }) => {
setTextScale('small');
});
expect(getLastHookValue().textScale).toEqual('small');
});
});
describe('textWrap state', () => {
it('has a default value', () => {
const { getLastHookValue } = mountHook(() => useLogViewConfiguration().textWrap);
expect(getLastHookValue()).toEqual(true);
});
it('can be updated', () => {
const { act, getLastHookValue } = mountHook(() => useLogViewConfiguration());
act(({ setTextWrap }) => {
setTextWrap(false);
});
expect(getLastHookValue().textWrap).toEqual(false);
});
});
it('provides the available text scales', () => {
const { getLastHookValue } = mountHook(() => useLogViewConfiguration().availableTextScales);
expect(getLastHookValue()).toEqual(expect.any(Array));
expect(getLastHookValue().length).toBeGreaterThan(0);
});
});

View file

@ -1,36 +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 createContainer from 'constate';
import { useState } from 'react';
export type TextScale = 'small' | 'medium' | 'large';
export const useLogViewConfiguration = () => {
// text scale
const [textScale, setTextScale] = useState<TextScale>('medium');
// text wrap
const [textWrap, setTextWrap] = useState<boolean>(true);
return {
availableTextScales,
setTextScale,
setTextWrap,
textScale,
textWrap,
};
};
export const [LogViewConfigurationProvider, useLogViewConfigurationContext] =
createContainer(useLogViewConfiguration);
/**
* constants
*/
export const availableTextScales: TextScale[] = ['large', 'medium', 'small'];

View file

@ -1,60 +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 { UrlStateContainer } from '../../utils/url_state';
import type { TextScale } from './log_view_configuration';
import { availableTextScales, useLogViewConfigurationContext } from './log_view_configuration';
interface LogTextviewUrlState {
textScale?: TextScale;
wrap?: boolean;
}
export const WithLogTextviewUrlState = () => {
const { textScale, textWrap, setTextScale, setTextWrap } = useLogViewConfigurationContext();
const urlState = useMemo(() => ({ textScale, wrap: textWrap }), [textScale, textWrap]);
return (
<UrlStateContainer
urlState={urlState}
urlStateKey="logTextview"
mapToUrlState={mapToUrlState}
onChange={(newUrlState) => {
if (newUrlState && newUrlState.textScale) {
setTextScale(newUrlState.textScale);
}
if (newUrlState && typeof newUrlState.wrap !== 'undefined') {
setTextWrap(newUrlState.wrap);
}
}}
onInitialize={(newUrlState) => {
if (newUrlState && newUrlState.textScale) {
setTextScale(newUrlState.textScale);
}
if (newUrlState && typeof newUrlState.wrap !== 'undefined') {
setTextWrap(newUrlState.wrap);
}
}}
/>
);
};
const mapToUrlState = (value: any): LogTextviewUrlState | undefined =>
value
? {
textScale: mapToTextScaleUrlState(value.textScale),
wrap: mapToWrapUrlState(value.wrap),
}
: undefined;
const mapToTextScaleUrlState = (value: any) =>
availableTextScales.includes(value) ? (value as TextScale) : undefined;
const mapToWrapUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined);

View file

@ -7,7 +7,7 @@
import { useEffect, useRef, useState } from 'react';
import type { OperatorFunction, PartialObserver, Subscription } from 'rxjs';
import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
import { switchMap } from 'rxjs';
export const useLatest = <Value>(value: Value) => {
@ -52,22 +52,6 @@ export const useBehaviorSubject = <
return [output$, next] as const;
};
export const useReplaySubject = <
InputValue,
OutputValue,
OutputObservable extends Observable<OutputValue>
>(
deriveObservableOnce: (input$: Observable<InputValue>) => OutputObservable
) => {
const [[subject$, next], _] = useState(() => {
const newSubject$ = new ReplaySubject<InputValue>();
const newNext = newSubject$.next.bind(newSubject$);
return [newSubject$, newNext] as const;
});
const [output$] = useState(() => deriveObservableOnce(subject$));
return [output$, next] as const;
};
export const useObservableState = <State, InitialState>(
state$: Observable<State>,
initialState: InitialState | (() => InitialState)

View file

@ -1,3 +0,0 @@
# @kbn/observability-logs-log-stream-page-state
The state machine of the Observability Logs UI stream page.

View file

@ -1,8 +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 './src';

View file

@ -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 * from './provider';
export * from './selectors';
export * from './state_machine';
export * from './types';

View file

@ -1,89 +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 { RefreshInterval } from '@kbn/data-plugin/public';
import type { InvokeCreator, Receiver } from 'xstate';
import type { TimeKey } from '../../../../../common/time';
import type { VisiblePositions } from '../../../log_stream_position_state';
import type { ExtendedTimeRange, ParsedQuery, Timestamps } from '../../../log_stream_query_state';
import type { LogStreamPageContext, LogStreamPageEvent } from './types';
export const waitForInitialQueryParameters =
(): InvokeCreator<LogStreamPageContext, LogStreamPageEvent> =>
(_context, _event) =>
(send, onEvent: Receiver<LogStreamPageEvent>) => {
// constituents of the set of initial parameters
let latestValidQuery: ParsedQuery | undefined;
let latestTimeRange: ExtendedTimeRange | undefined;
let latestRefreshInterval: RefreshInterval | undefined;
let latestTimestamps: Timestamps | undefined;
onEvent((event) => {
switch (event.type) {
// event types that deliver the parameters
case 'VALID_QUERY_CHANGED':
case 'INVALID_QUERY_CHANGED':
latestValidQuery = event.parsedQuery;
break;
case 'TIME_CHANGED':
latestTimeRange = event.timeRange;
latestRefreshInterval = event.refreshInterval;
latestTimestamps = event.timestamps;
break;
}
// if all constituents of the parameters have been delivered
if (
latestValidQuery !== undefined &&
latestTimeRange !== undefined &&
latestRefreshInterval !== undefined &&
latestTimestamps !== undefined
) {
send({
type: 'RECEIVED_INITIAL_QUERY_PARAMETERS',
validatedQuery: latestValidQuery,
timeRange: latestTimeRange,
refreshInterval: latestRefreshInterval,
timestamps: latestTimestamps,
});
}
});
};
export const waitForInitialPositionParameters =
(): InvokeCreator<LogStreamPageContext, LogStreamPageEvent> =>
(_context, _event) =>
(send, onEvent: Receiver<LogStreamPageEvent>) => {
// constituents of the set of initial parameters
let latestTargetPosition: TimeKey | null;
let latestLatestPosition: TimeKey | null;
let latestVisiblePositions: VisiblePositions;
onEvent((event) => {
switch (event.type) {
case 'POSITIONS_CHANGED':
latestTargetPosition = event.targetPosition;
latestLatestPosition = event.latestPosition;
latestVisiblePositions = event.visiblePositions;
break;
}
// if all constituents of the parameters have been delivered
if (
latestTargetPosition !== undefined &&
latestLatestPosition !== undefined &&
latestVisiblePositions !== undefined
) {
send({
type: 'RECEIVED_INITIAL_POSITION_PARAMETERS',
targetPosition: latestTargetPosition,
latestPosition: latestLatestPosition,
visiblePositions: latestVisiblePositions,
});
}
});
};

View file

@ -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 { useInterpret } from '@xstate/react';
import createContainer from 'constate';
import useMount from 'react-use/lib/useMount';
import { isDevMode } from '../../../../utils/dev_mode';
import {
createLogStreamPageStateMachine,
type LogStreamPageStateMachineDependencies,
} from './state_machine';
export const useLogStreamPageState = ({
kibanaQuerySettings,
logViewStateNotifications,
queryStringService,
toastsService,
filterManagerService,
urlStateStorage,
useDevTools = isDevMode(),
timeFilterService,
}: {
useDevTools?: boolean;
} & LogStreamPageStateMachineDependencies) => {
useMount(() => {
// eslint-disable-next-line no-console
console.log(
"A warning in console stating: 'The result of getSnapshot should be cached to avoid an infinite loop' is expected. This will be fixed once we can upgrade versions."
);
});
const logStreamPageStateService = useInterpret(
() =>
createLogStreamPageStateMachine({
kibanaQuerySettings,
logViewStateNotifications,
queryStringService,
toastsService,
filterManagerService,
urlStateStorage,
timeFilterService,
}),
{ devTools: useDevTools }
);
return logStreamPageStateService;
};
export const [LogStreamPageStateProvider, useLogStreamPageStateContext] =
createContainer(useLogStreamPageState);

View file

@ -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.
*/
import type { MatchedStateFromActor } from '@kbn/xstate-utils';
import type { LogStreamQueryActorRef } from '../../../log_stream_query_state';
import type { LogStreamPageActorRef } from './state_machine';
type LogStreamPageStateWithLogViewIndices =
| MatchedStateFromActor<LogStreamPageActorRef, 'hasLogViewIndices'>
| MatchedStateFromActor<LogStreamPageActorRef, { hasLogViewIndices: 'initialized' }>
| MatchedStateFromActor<LogStreamPageActorRef, { hasLogViewIndices: 'uninitialized' }>;
export const selectLogStreamQueryChildService = (
state: LogStreamPageStateWithLogViewIndices
): LogStreamQueryActorRef => state.children.logStreamQuery;

View file

@ -1,355 +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 { RefreshInterval } from '@kbn/data-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import type { ActorRefFrom, EmittedFrom } from 'xstate';
import { actions, createMachine } from 'xstate';
import { DEFAULT_REFRESH_INTERVAL } from '@kbn/logs-shared-plugin/common';
import type { LogViewNotificationChannel } from '@kbn/logs-shared-plugin/public';
import type { OmitDeprecatedState } from '@kbn/xstate-utils';
import { datemathToEpochMillis } from '../../../../utils/datemath';
import { createLogStreamPositionStateMachine } from '../../../log_stream_position_state/src/state_machine';
import type { LogStreamQueryStateMachineDependencies } from '../../../log_stream_query_state';
import {
createLogStreamQueryStateMachine,
DEFAULT_TIMERANGE,
} from '../../../log_stream_query_state';
import {
waitForInitialQueryParameters,
waitForInitialPositionParameters,
} from './initial_parameters_service';
import type {
LogStreamPageContext,
LogStreamPageContextWithLogView,
LogStreamPageContextWithLogViewError,
LogStreamPageContextWithPositions,
LogStreamPageContextWithQuery,
LogStreamPageContextWithTime,
LogStreamPageEvent,
LogStreamPageTypestate,
} from './types';
export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPageContext = {}) =>
/** @xstate-layout N4IgpgJg5mDOIC5QBsD2UDKAXATmAhgLYAK+M2+WYAdAK4B2Alk1o-sowF6QDEAMgHkAggBEAkgDkA4gH1BsgGpiAogHUZGACpCASpuUiA2gAYAuolAAHVLEatU9CyAAeiALQAmAJzUPHgGwArIEALAAcHmHhXsEhADQgAJ6IAQDM1IHGYQDs2ZnZxl5e-l6pAL5lCWiYuAQkZGAUVHRMLGwc3BD8wuLScgKKKuoAYkJifAYm5kgg1rb2jjOuCJ4hAIzUqZklHgX+xqlrkQnJCKlB1P4loYH+qV7hHoEVVejYeESk5FiUNAzMdnaXF4glEklk8hkSjUGgAqgBheHKAyTMxOOaAhxOZbZNbZajGXLBVIkrJrYInRBHYzUEIeVIhVJhNZZLws7IhF4garvOpfRo-Zr-NrsYFdUG9CEDKFDOGI5EiSZraZWGyYxagHEhEIEwkeI7Zc7GO6UhCZHzZML7MKBDwhQp0zmVblvWqfBpNGhofAQZhQPjoBSMMAAd26YL6kOhIzGEyMaJmGIW2JS4VpJTCWXp5K82Q8pvN1Et1tt9oedq5PLd9W+v2o3t99H9geDYYl4P6gxhGARSJR8ZVszVyaWiEZPnt-m8Ry8xjta38pqN1FK+uy+w8xhCgS8lddHxrArrDb9AagQdD4clnZl3d7CqVg6TjCxo4QISumy2MWNMWiAVNO58WMFkImMOc50CMI9xqA9+U9etUB9U8W1DYZ8EYZAQR6Dso1lLRdH0Ad0WHF8NRcdxvF8AJYgiKIwhiUIC1SDwMgNcC8g5O1nmdKs4I9QUaAAC3wWAzwvEMxHoX0AGM4GoAFWFFTg-QARVoMAcESHgdGUJExAUAwZEkMRNDEIQ+BkVTYWUHQAE0ZGIXQhAAWWUfQdAwKYSPmMinFOKcNgKXYwnOPMbRYhJlhCYoYN5d1a2aESxNQyTpMYOTYAUkUOjUjStJ4BQLLEEQrJs+yZHhAAJIRpFRJ9SNfTUx2KAtDSLOdLTCyJAhYuLq3gwTqGS8TWyk2T5MUoEVKbdTNO0yQir4EqytshzqtqqR6p89UU3fVqkkQS1WNKXEoP8cLeo8fr+MS4TRNG0Nxoyyacq4PL5p4My3Mqmq6uIxNGvI6KDtOW1Ag6kLuoi67eP3PkBLrEbUuezLssBZS-WIIHYB0vTlAMoyTLMizHIEDBTLEAQJEc5y3I8ryE1VXymoo-bF0OhAGI2EDbVCi6er6uHYIRu7hoelH0rRqbMabbGWfoXHiHJynqYwX7Nu2wGFb2mKOdOIofEKBksgFmGbtFo8kol88xql16MY6XglpW6y1o1-7vO13a3wXPJaTWCJbSne0SQLOcV3XJ5t0yXq1jWC2Eqt+6Uttp77aymWna6b7lA9raAeZn3moQBdCV8b99hiQ0rnzTntx1XMpxuWPDgT4X4sPBDkbTtKJszt7Oh4ZWKbMtX861ouRxLsv8XpHcq8CGup0A+OCVCVJskY4w49h14RaT7ubYk1GHaU7OeAAKVhFziBkTQBHv3Qts0MnR6piQvanvzff2OfK8KEvc4K967Gkjs3GOO826Jy7kNHuJ8M7o3PmKPGys9AygpgAIQmG-VWEhGYNR1r-cu89iiAOXnXU4WwwjgOjsEKB8cYGDSRsfO2-ckHTV4LCYgIghD6HvmIH6OhNZfyHEQmef8K4L3IcAyhR01g+DWCxTIkDd5MMRtbVOCD2FZxQdw3h-DdLDF0hgKqxkJAeSWqI58rNlizykWQ6usilwsloS3Bh7d96d2YZox6fcXoD0digpyW0ZDKAkKVTBsJhjDFsjIXSQhqqTzEcXNm9jSGLwoaaAIJ0o7uLju3Z09BUAQDgE4PiltPQ7WnmzTwDFNjbC8LsY0BwlGmjcCxAk9IOSZgXDuUotx1Fi2FEEzo1Sf4lzpKaNYdJqDknAlkG0bIghbiGcnRCyEmx+PGbYlI+JYhXCtDaXIP54icyZDQ+4-gjgCy2H4HiXiBoaK9EhRszZe7oUwpAHZwNEAQ2ZHaJpdwLpsiWYBHcRZN72njlubw0EO5PLFvAthASfl7TcOEaZEcQhR3WDaGKXVPEugPrAlhWiUXS0Hh9LSaK3zGEAvqSGXUzZXTWUfcl6cdFUrljjWlJd6WcwiKxTcUMWVC0ebddZyLOUBI4cpb53sal2KZDqPI5IllANrgWJ4TL+aXXFcS7xzzqCEEYLAWwWzJb9z5Wkw4-hfCFD8NvG0F1wUWk3pvXEXqDjlAqGUIAA */
createMachine<LogStreamPageContext, LogStreamPageEvent, LogStreamPageTypestate>(
{
context: initialContext,
predictableActionArguments: true,
invoke: {
src: 'logViewNotifications',
},
id: 'logStreamPageState',
initial: 'uninitialized',
states: {
uninitialized: {
on: {
LOADING_LOG_VIEW_STARTED: {
target: 'loadingLogView',
},
LOADING_LOG_VIEW_FAILED: {
target: 'loadingLogViewFailed',
actions: 'storeLogViewError',
},
LOADING_LOG_VIEW_SUCCEEDED: [
{
target: 'hasLogViewIndices',
cond: 'hasLogViewIndices',
actions: 'storeResolvedLogView',
},
{
target: 'missingLogViewIndices',
actions: 'storeResolvedLogView',
},
],
},
},
loadingLogView: {
on: {
LOADING_LOG_VIEW_FAILED: {
target: 'loadingLogViewFailed',
actions: 'storeLogViewError',
},
LOADING_LOG_VIEW_SUCCEEDED: [
{
target: 'hasLogViewIndices',
cond: 'hasLogViewIndices',
actions: 'storeResolvedLogView',
},
{
target: 'missingLogViewIndices',
actions: 'storeResolvedLogView',
},
],
},
},
loadingLogViewFailed: {
on: {
LOADING_LOG_VIEW_STARTED: {
target: 'loadingLogView',
},
},
},
hasLogViewIndices: {
initial: 'initializingQuery',
states: {
initializingQuery: {
meta: {
_DX_warning_:
"The Query machine must be invoked and complete initialisation before the Position machine is invoked. This is due to legacy URL dependencies on the 'logPosition' key, we need to read the key before it is reset by the Position machine.",
},
invoke: {
src: 'waitForInitialQueryParameters',
id: 'waitForInitialQueryParameters',
},
on: {
RECEIVED_INITIAL_QUERY_PARAMETERS: {
target: 'initializingPositions',
actions: ['storeQuery', 'storeTime', 'forwardToLogPosition'],
},
VALID_QUERY_CHANGED: {
target: 'initializingQuery',
internal: true,
actions: 'forwardToInitialQueryParameters',
},
INVALID_QUERY_CHANGED: {
target: 'initializingQuery',
internal: true,
actions: 'forwardToInitialQueryParameters',
},
TIME_CHANGED: {
target: 'initializingQuery',
internal: true,
actions: 'forwardToInitialQueryParameters',
},
},
},
initializingPositions: {
meta: {
_DX_warning_:
"The Position machine must be invoked after the Query machine has been invoked and completed initialisation. This is due to the Query machine having some legacy URL dependencies on the 'logPosition' key, we don't want the Position machine to reset the URL parameters before the Query machine has had a chance to read them.",
},
invoke: [
{
src: 'waitForInitialPositionParameters',
id: 'waitForInitialPositionParameters',
},
],
on: {
RECEIVED_INITIAL_POSITION_PARAMETERS: {
target: 'initialized',
actions: ['storePositions'],
},
POSITIONS_CHANGED: {
target: 'initializingPositions',
internal: true,
actions: 'forwardToInitialPositionParameters',
},
},
},
initialized: {
on: {
VALID_QUERY_CHANGED: {
target: 'initialized',
internal: true,
actions: 'storeQuery',
},
TIME_CHANGED: {
target: 'initialized',
internal: true,
actions: ['storeTime', 'forwardToLogPosition'],
},
POSITIONS_CHANGED: {
target: 'initialized',
internal: true,
actions: ['storePositions'],
},
JUMP_TO_TARGET_POSITION: {
target: 'initialized',
internal: true,
actions: ['forwardToLogPosition'],
},
REPORT_VISIBLE_POSITIONS: {
target: 'initialized',
internal: true,
actions: ['forwardToLogPosition'],
},
UPDATE_TIME_RANGE: {
target: 'initialized',
internal: true,
actions: ['forwardToLogStreamQuery'],
},
UPDATE_REFRESH_INTERVAL: {
target: 'initialized',
internal: true,
actions: ['forwardToLogStreamQuery'],
},
PAGE_END_BUFFER_REACHED: {
target: 'initialized',
internal: true,
actions: ['forwardToLogStreamQuery'],
},
},
},
},
invoke: [
{
src: 'logStreamQuery',
id: 'logStreamQuery',
},
{
src: 'logStreamPosition',
id: 'logStreamPosition',
},
],
},
missingLogViewIndices: {},
},
},
{
actions: {
forwardToInitialQueryParameters: actions.forwardTo('waitForInitialQueryParameters'),
forwardToInitialPositionParameters: actions.forwardTo('waitForInitialPositionParameters'),
forwardToLogPosition: actions.forwardTo('logStreamPosition'),
forwardToLogStreamQuery: actions.forwardTo('logStreamQuery'),
storeLogViewError: actions.assign((_context, event) =>
event.type === 'LOADING_LOG_VIEW_FAILED'
? ({ logViewError: event.error } as LogStreamPageContextWithLogViewError)
: {}
),
storeResolvedLogView: actions.assign((_context, event) =>
event.type === 'LOADING_LOG_VIEW_SUCCEEDED'
? ({
logViewStatus: event.status,
resolvedLogView: event.resolvedLogView,
} as LogStreamPageContextWithLogView)
: {}
),
storeQuery: actions.assign((_context, event) =>
event.type === 'RECEIVED_INITIAL_QUERY_PARAMETERS'
? ({
parsedQuery: event.validatedQuery,
} as LogStreamPageContextWithQuery)
: event.type === 'VALID_QUERY_CHANGED'
? ({
parsedQuery: event.parsedQuery,
} as LogStreamPageContextWithQuery)
: {}
),
storeTime: actions.assign((_context, event) => {
return 'timeRange' in event && 'refreshInterval' in event && 'timestamps' in event
? ({
timeRange: event.timeRange,
refreshInterval: event.refreshInterval,
timestamps: event.timestamps,
} as LogStreamPageContextWithTime)
: {};
}),
storePositions: actions.assign((_context, event) => {
return 'targetPosition' in event &&
'visiblePositions' in event &&
'latestPosition' in event
? ({
targetPosition: event.targetPosition,
visiblePositions: event.visiblePositions,
latestPosition: event.latestPosition,
} as LogStreamPageContextWithPositions)
: {};
}),
},
guards: {
hasLogViewIndices: (_context, event) =>
event.type === 'LOADING_LOG_VIEW_SUCCEEDED' &&
['empty', 'available'].includes(event.status.index),
},
}
);
export type LogStreamPageStateMachine = ReturnType<typeof createPureLogStreamPageStateMachine>;
export type LogStreamPageActorRef = OmitDeprecatedState<ActorRefFrom<LogStreamPageStateMachine>>;
export type LogStreamPageState = EmittedFrom<LogStreamPageActorRef>;
export type LogStreamPageSend = LogStreamPageActorRef['send'];
export type LogStreamPageStateMachineDependencies = {
logViewStateNotifications: LogViewNotificationChannel;
} & LogStreamQueryStateMachineDependencies;
export const createLogStreamPageStateMachine = ({
kibanaQuerySettings,
logViewStateNotifications,
queryStringService,
toastsService,
filterManagerService,
urlStateStorage,
timeFilterService,
}: LogStreamPageStateMachineDependencies) =>
createPureLogStreamPageStateMachine().withConfig({
services: {
logViewNotifications: () => logViewStateNotifications.createService(),
logStreamQuery: (context) => {
if (!('resolvedLogView' in context)) {
throw new Error('Failed to spawn log stream query service: no LogView in context');
}
const nowTimestamp = Date.now();
const initialTimeRangeExpression: TimeRange = DEFAULT_TIMERANGE;
const initialRefreshInterval: RefreshInterval = DEFAULT_REFRESH_INTERVAL;
return createLogStreamQueryStateMachine(
{
dataViews: [context.resolvedLogView.dataViewReference],
timeRange: {
...initialTimeRangeExpression,
lastChangedCompletely: nowTimestamp,
},
timestamps: {
startTimestamp: datemathToEpochMillis(initialTimeRangeExpression.from, 'down') ?? 0,
endTimestamp: datemathToEpochMillis(initialTimeRangeExpression.to, 'up') ?? 0,
lastChangedTimestamp: nowTimestamp,
},
refreshInterval: initialRefreshInterval,
},
{
kibanaQuerySettings,
queryStringService,
toastsService,
filterManagerService,
urlStateStorage,
timeFilterService,
}
);
},
logStreamPosition: (context) => {
return createLogStreamPositionStateMachine(
{
targetPosition: null,
latestPosition: null,
visiblePositions: {
endKey: null,
middleKey: null,
startKey: null,
pagesBeforeStart: Infinity,
pagesAfterEnd: Infinity,
},
},
{
urlStateStorage,
toastsService,
}
);
},
waitForInitialQueryParameters: waitForInitialQueryParameters(),
waitForInitialPositionParameters: waitForInitialPositionParameters(),
},
});

View file

@ -1,116 +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 { TimeRange } from '@kbn/es-query';
import type { LogViewStatus } from '@kbn/logs-shared-plugin/common';
import type {
LogViewContextWithError,
LogViewContextWithResolvedLogView,
LogViewNotificationEvent,
} from '@kbn/logs-shared-plugin/public';
import type { TimeKey } from '../../../../../common/time';
import type {
JumpToTargetPositionEvent,
LogStreamPositionContext,
ReportVisiblePositionsEvent,
VisiblePositions,
} from '../../../log_stream_position_state';
import type { LogStreamPositionNotificationEvent } from '../../../log_stream_position_state/src/notifications';
import type {
LogStreamQueryContextWithTime,
ParsedQuery,
UpdateRefreshIntervalEvent,
UpdateTimeRangeEvent,
} from '../../../log_stream_query_state';
import type { LogStreamQueryNotificationEvent } from '../../../log_stream_query_state/src/notifications';
export interface ReceivedInitialQueryParametersEvent {
type: 'RECEIVED_INITIAL_QUERY_PARAMETERS';
validatedQuery: ParsedQuery;
timeRange: LogStreamPageContextWithTime['timeRange'];
refreshInterval: LogStreamPageContextWithTime['refreshInterval'];
timestamps: LogStreamPageContextWithTime['timestamps'];
}
export interface ReceivedInitialPositionParametersEvent {
type: 'RECEIVED_INITIAL_POSITION_PARAMETERS';
targetPosition: LogStreamPageContextWithPositions['targetPosition'];
latestPosition: LogStreamPageContextWithPositions['latestPosition'];
visiblePositions: LogStreamPageContextWithPositions['visiblePositions'];
}
export type LogStreamPageEvent =
| LogViewNotificationEvent
| LogStreamQueryNotificationEvent
| LogStreamPositionNotificationEvent
| ReceivedInitialQueryParametersEvent
| ReceivedInitialPositionParametersEvent
| JumpToTargetPositionEvent
| ReportVisiblePositionsEvent
| UpdateTimeRangeEvent
| UpdateRefreshIntervalEvent;
export interface LogStreamPageContextWithLogView {
logViewStatus: LogViewStatus;
resolvedLogView: LogViewContextWithResolvedLogView['resolvedLogView'];
}
export interface LogStreamPageContextWithLogViewError {
logViewError: LogViewContextWithError['error'];
}
export interface LogStreamPageContextWithQuery {
parsedQuery: ParsedQuery;
}
export type LogStreamPageContextWithTime = LogStreamQueryContextWithTime;
export type LogStreamPageContextWithPositions = LogStreamPositionContext;
export type LogStreamPageTypestate =
| {
value: 'uninitialized';
context: {};
}
| {
value: 'loadingLogView';
context: {};
}
| {
value: 'loadingLogViewFailed';
context: LogStreamPageContextWithLogViewError;
}
| {
value: 'hasLogViewIndices';
context: LogStreamPageContextWithLogView;
}
| {
value: { hasLogViewIndices: 'uninitialized' };
context: LogStreamPageContextWithLogView;
}
| {
value: { hasLogViewIndices: 'initialized' };
context: LogStreamPageContextWithLogView &
LogStreamPageContextWithQuery &
LogStreamPageContextWithTime &
LogStreamPageContextWithPositions;
}
| {
value: 'missingLogViewIndices';
context: LogStreamPageContextWithLogView;
};
export type LogStreamPageStateValue = LogStreamPageTypestate['value'];
export type LogStreamPageContext = LogStreamPageTypestate['context'];
export interface LogStreamPageCallbacks {
updateTimeRange: (timeRange: Partial<TimeRange>) => void;
jumpToTargetPosition: (targetPosition: TimeKey | null) => void;
jumpToTargetPositionTime: (time: string) => void;
reportVisiblePositions: (visiblePositions: VisiblePositions) => void;
startLiveStreaming: () => void;
stopLiveStreaming: () => void;
}

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './src/types';
export * from './src/defaults';

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const DESIRED_BUFFER_PAGES = 2;
export const RELATIVE_END_UPDATE_DELAY = 1000;

View file

@ -1,44 +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 {
LogStreamPositionContext,
LogStreamPositionContextWithLatestPosition,
LogStreamPositionContextWithTargetPosition,
LogStreamPositionContextWithVisiblePositions,
} from './types';
export type PositionsChangedEvent = {
type: 'POSITIONS_CHANGED';
} & LogStreamPositionContextWithTargetPosition &
LogStreamPositionContextWithLatestPosition &
LogStreamPositionContextWithVisiblePositions;
export interface PageEndBufferReachedEvent {
type: 'PAGE_END_BUFFER_REACHED';
}
export type LogStreamPositionNotificationEvent = PositionsChangedEvent | PageEndBufferReachedEvent;
export const LogStreamPositionNotificationEventSelectors = {
positionsChanged: (context: LogStreamPositionContext) => {
return 'targetPosition' in context &&
'latestPosition' in context &&
'visiblePositions' in context
? ({
type: 'POSITIONS_CHANGED',
targetPosition: context.targetPosition,
latestPosition: context.latestPosition,
visiblePositions: context.visiblePositions,
} as LogStreamPositionNotificationEvent)
: undefined;
},
pageEndBufferReached: (context: LogStreamPositionContext) =>
({
type: 'PAGE_END_BUFFER_REACHED',
} as LogStreamPositionNotificationEvent),
};

View file

@ -1,241 +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 { IToasts } from '@kbn/core-notifications-browser';
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { convertISODateToNanoPrecision } from '@kbn/logs-shared-plugin/common';
import moment from 'moment';
import type { ActorRefFrom, EmittedFrom } from 'xstate';
import { actions, createMachine, SpecialTargets } from 'xstate';
import type { OmitDeprecatedState } from '@kbn/xstate-utils';
import { sendIfDefined } from '@kbn/xstate-utils';
import { isSameTimeKey } from '../../../../common/time';
import { DESIRED_BUFFER_PAGES, RELATIVE_END_UPDATE_DELAY } from './defaults';
import { LogStreamPositionNotificationEventSelectors } from './notifications';
import type {
LogStreamPositionContext,
LogStreamPositionContextWithLatestPosition,
LogStreamPositionContextWithTargetPosition,
LogStreamPositionContextWithVisiblePositions,
LogStreamPositionEvent,
LogStreamPositionTypestate,
} from './types';
import { initializeFromUrl, updateContextInUrl } from './url_state_storage_service';
export const createPureLogStreamPositionStateMachine = (initialContext: LogStreamPositionContext) =>
/** @xstate-layout N4IgpgJg5mDOIC5QBsD2UDKAXATmAhgLYAKqsAlluagHbb5ZgB0Arjee1fsuQF6QBiAEoBRAMIiAkgDURAEQD6kgHKSAKpICCAGQUBFAKoihATQXFNQzQFkRa4xgDaABgC6iUAAcylajQ8gAB6IALQAbACsTAAcAEzRzgCcACzJccmxAIzR0QA0IACeiNkAzExhJSUA7FURsSXRYYklEVUAvm35aJi4BCQ+VLT0jEwcvtx8HFAAYjiohAY4yAIq6lrakgBa8grTQgDy1goGQtou7kgg3hSD-pfBCCGxTBHREYnOTelZ0bX5RQg0okmD8miVMhEwmEqmFGh0uuhsHgiKQbn5hswxlwePwIExrr5aLBRpxyBNcQIAFIGazEBRqfb0ywAcTs5n2GDW+2U5wCBNuAQeVUSz2SULCmQ+zlScQi-0QyWcVSYVU+QIhzlejQi8JA3SRfVRhLoWAYmNJ5Mg+IGfmJWLJOMEomI+yEagU0kknIAQtoROzORpuU43HybbRBYg6mEmGloskqrEqjkwrFYvLAdEyhkItLMlVqtVc+1OnrEb0UeGTWaSeNHXj+bba9i+IINLYFGIABKaZSsuS8y6NiP3RDC57ChOpZzStKxOWFRBZMqpqr5xIpT6xEXRXX6iv9NFDU0je2WvFYAAWcywWB4NCgxHwMBENAgylQVAAZuQAMYMJtyAgZAwGEEQXTdD0vUkX1-RdQNJGDQcvCrSMEHeTJygSWJkg+X5FQhDN4xjNNIk1V4xRKWIwj3ctkUPY0MWbB1Wwva9PzvKYnxfN8P2-P8AKJJgrxvTiHzAiD3U9H0-QDLllBDC4UKPO5QAeTJJTKVVITTRpt3iDMaiiCINUSDTMlw-Nklonp6KNW4mLPethPY2970fZ8wFfd9P3IH9-1uYkRI49yBECWAT2YfAv0YHAAApRG0TQNFkBQRGURQDGIORkv9OQRCSkwAEoBH3Oyq0ci1nOCtyuM87y+L8gTApc0T3OQq5UNHBAzOiFVohFEo5xKZx6mSDNeqYDTakyVMlWiTIhpsg1KxUyq61Y1qQrqnifP4gKmxqsSoDCiKa2i2KEoK5KZH9dLMuy3KFHywqSrKw0Ksi5jzy22qH24rzeN8-zBJoILXOOxxMiUzqVLQ2plTqEoRWnGcUgmpopvjRUYWSbJWhKZaD3s9EvqczajvcgGGuB5qmxoYGCimAQOuHVSggVGpymhMJnAhVUEjqDNMiVKJaiG+dfgG6prN1BmIDgAJ3tWxjIrDOHupCDSyjiBIUjnDJsjyRdHjSGJEwWjd8zeNGifKtavrYcncXV400IyIjMI3b36lVXmIgTO2PodmtnamWZ5kWZBXYFTXnmlWE-hNka+uoxIA6zRJZUJ0tlYYhyyaq1iY78NCSkiFULLnSFkYlaEMwacok2yGcRZF1pMiDlWC9DovcWtFT4CHLq1IVRaYnjapEmTRNnHBYXqKm5vhWaBIJTqLv89J3uNv7tm7T7yAS5HUfAQlCfkinmfYjnzIM23KI04z5Hs83knjx3lt+8pnbAb2pqDpEmPuzB4eMtJV1lBURIdcqgZlhH1PmJkcJDTqItEsCJbLB1Vp-Fi38IZU3qkDfaoM7TATAMA92581wynnFAmBRExQgmormCIFEKjUTfp9HBP0f7-UIf-EGLVeFQAod1cyzx4isKqHOfMkIGEkWYeRYiVEaK5zolgnup5D5sTar-GmxCWoM2-EzB8ojT5t2BK8HClFFqREIibduzgVQRCGiNJUyRWEig6B0IAA */
createMachine<LogStreamPositionContext, LogStreamPositionEvent, LogStreamPositionTypestate>(
{
context: initialContext,
predictableActionArguments: true,
id: 'logStreamPositionState',
initial: 'uninitialized',
states: {
uninitialized: {
meta: {
_DX_warning_:
"The Position machine cannot initializeFromUrl until after the Query machine has initialized, this is due to a dual dependency on the 'logPosition' URL parameter for legacy reasons.",
},
on: {
RECEIVED_INITIAL_QUERY_PARAMETERS: {
target: 'initializingFromUrl',
},
},
},
initializingFromUrl: {
on: {
INITIALIZED_FROM_URL: [
{
target: 'initialized',
actions: ['storeTargetPosition', 'storeLatestPosition'],
},
],
},
invoke: {
src: 'initializeFromUrl',
},
},
initialized: {
type: 'parallel',
states: {
positions: {
initial: 'initialized',
states: {
initialized: {
entry: ['updateContextInUrl', 'notifyPositionsChanged'],
on: {
JUMP_TO_TARGET_POSITION: {
target: 'initialized',
actions: ['updateTargetPosition'],
},
REPORT_VISIBLE_POSITIONS: {
target: 'initialized',
actions: ['updateVisiblePositions'],
},
TIME_CHANGED: {
target: 'initialized',
actions: ['updatePositionsFromTimeChange'],
},
},
},
},
},
throttlingPageEndNotifications: {
initial: 'idle',
states: {
idle: {
on: {
REPORT_VISIBLE_POSITIONS: {
target: 'throttling',
},
},
},
throttling: {
after: {
RELATIVE_END_UPDATE_DELAY: [
{
target: 'notifying',
cond: 'hasReachedPageEndBuffer',
},
{
target: 'idle',
},
],
},
on: {
REPORT_VISIBLE_POSITIONS: {
target: 'throttling',
},
},
},
notifying: {
entry: ['notifyPageEndBufferReached'],
always: 'idle',
},
},
},
},
},
},
},
{
actions: {
notifyPositionsChanged: actions.pure(() => undefined),
notifyPageEndBufferReached: actions.pure(() => undefined),
storeTargetPosition: actions.assign((_context, event) =>
'targetPosition' in event
? ({
targetPosition: event.targetPosition,
} as LogStreamPositionContextWithTargetPosition)
: {}
),
storeLatestPosition: actions.assign((_context, event) =>
'latestPosition' in event
? ({
latestPosition: event.latestPosition,
} as LogStreamPositionContextWithLatestPosition)
: {}
),
updateTargetPosition: actions.assign((_context, event) => {
if (!('targetPosition' in event)) return {};
const nextTargetPosition = event.targetPosition?.time
? {
time: event.targetPosition.time,
tiebreaker: event.targetPosition.tiebreaker ?? 0,
}
: null;
const nextLatestPosition = !isSameTimeKey(_context.targetPosition, nextTargetPosition)
? nextTargetPosition
: _context.latestPosition;
return {
targetPosition: nextTargetPosition,
latestPosition: nextLatestPosition,
} as LogStreamPositionContextWithLatestPosition &
LogStreamPositionContextWithTargetPosition;
}),
updatePositionsFromTimeChange: actions.assign((_context, event) => {
if (!('timeRange' in event)) return {};
const {
timestamps: { startTimestamp, endTimestamp },
} = event;
// Reset the target position if it doesn't fall within the new range.
const targetPositionNanoTime =
_context.targetPosition && convertISODateToNanoPrecision(_context.targetPosition.time);
const startNanoDate = convertISODateToNanoPrecision(moment(startTimestamp).toISOString());
const endNanoDate = convertISODateToNanoPrecision(moment(endTimestamp).toISOString());
const targetPositionShouldReset =
targetPositionNanoTime &&
(startNanoDate > targetPositionNanoTime || endNanoDate < targetPositionNanoTime);
return {
targetPosition: targetPositionShouldReset ? null : _context.targetPosition,
latestPosition: targetPositionShouldReset ? null : _context.latestPosition,
} as LogStreamPositionContextWithLatestPosition &
LogStreamPositionContextWithTargetPosition;
}),
updateVisiblePositions: actions.assign((_context, event) =>
'visiblePositions' in event
? ({
visiblePositions: event.visiblePositions,
latestPosition: !isSameTimeKey(
_context.visiblePositions.middleKey,
event.visiblePositions.middleKey
)
? event.visiblePositions.middleKey
: _context.visiblePositions.middleKey,
} as LogStreamPositionContextWithVisiblePositions)
: {}
),
},
delays: {
RELATIVE_END_UPDATE_DELAY,
},
guards: {
// User is close to the bottom of the page.
hasReachedPageEndBuffer: (context, event) =>
context.visiblePositions.pagesAfterEnd < DESIRED_BUFFER_PAGES,
},
}
);
export type LogStreamPositionStateMachine = ReturnType<
typeof createPureLogStreamPositionStateMachine
>;
export type LogStreamPositionActorRef = OmitDeprecatedState<
ActorRefFrom<LogStreamPositionStateMachine>
>;
export type LogStreamPositionState = EmittedFrom<LogStreamPositionActorRef>;
export interface LogStreamPositionStateMachineDependencies {
urlStateStorage: IKbnUrlStateStorage;
toastsService: IToasts;
}
export const createLogStreamPositionStateMachine = (
initialContext: LogStreamPositionContext,
{ urlStateStorage, toastsService }: LogStreamPositionStateMachineDependencies
) =>
createPureLogStreamPositionStateMachine(initialContext).withConfig({
actions: {
updateContextInUrl: updateContextInUrl({ toastsService, urlStateStorage }),
notifyPositionsChanged: sendIfDefined(SpecialTargets.Parent)(
LogStreamPositionNotificationEventSelectors.positionsChanged
),
notifyPageEndBufferReached: sendIfDefined(SpecialTargets.Parent)(
LogStreamPositionNotificationEventSelectors.pageEndBufferReached
),
},
services: {
initializeFromUrl: initializeFromUrl({ toastsService, urlStateStorage }),
},
});

View file

@ -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 type { TimeKey } from '../../../../common/time';
import type { ReceivedInitialQueryParametersEvent } from '../../log_stream_page/state';
import type { TimeChangedEvent } from '../../log_stream_query_state/src/notifications';
export interface VisiblePositions {
startKey: TimeKey | null;
middleKey: TimeKey | null;
endKey: TimeKey | null;
pagesAfterEnd: number;
pagesBeforeStart: number;
}
export interface LogStreamPositionContextWithTargetPosition {
targetPosition: TimeKey | null;
}
export interface LogStreamPositionContextWithLatestPosition {
latestPosition: TimeKey | null;
}
export interface LogStreamPositionContextWithVisiblePositions {
visiblePositions: VisiblePositions;
}
export type LogStreamPositionState = LogStreamPositionContextWithTargetPosition &
LogStreamPositionContextWithLatestPosition &
LogStreamPositionContextWithVisiblePositions;
export type LogStreamPositionTypestate =
| {
value: 'uninitialized';
context: LogStreamPositionState;
}
| {
value: 'initialized';
context: LogStreamPositionState;
};
export type LogStreamPositionContext = LogStreamPositionTypestate['context'];
export type LogStreamPositionStateValue = LogStreamPositionTypestate['value'];
export interface JumpToTargetPositionEvent {
type: 'JUMP_TO_TARGET_POSITION';
targetPosition: Partial<TimeKey> | null;
}
export interface ReportVisiblePositionsEvent {
type: 'REPORT_VISIBLE_POSITIONS';
visiblePositions: VisiblePositions;
}
export type LogStreamPositionEvent =
| {
type: 'INITIALIZED_FROM_URL';
latestPosition: TimeKey | null;
targetPosition: TimeKey | null;
}
| ReceivedInitialQueryParametersEvent
| JumpToTargetPositionEvent
| ReportVisiblePositionsEvent
| TimeChangedEvent;

View file

@ -1,100 +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 * as rt from 'io-ts';
import type { IToasts } from '@kbn/core-notifications-browser';
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
import * as Either from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';
import type { InvokeCreator } from 'xstate';
import { createPlainError, formatErrors } from '@kbn/io-ts-utils';
import { minimalTimeKeyRT, pickTimeKey } from '../../../../common/time';
import type { LogStreamPositionContext, LogStreamPositionEvent } from './types';
interface LogStreamPositionUrlStateDependencies {
positionStateKey?: string;
toastsService: IToasts;
urlStateStorage: IKbnUrlStateStorage;
}
export const defaultPositionStateKey = 'logPosition';
export const updateContextInUrl =
({
urlStateStorage,
positionStateKey = defaultPositionStateKey,
}: LogStreamPositionUrlStateDependencies) =>
(context: LogStreamPositionContext, _event: LogStreamPositionEvent) => {
if (!('latestPosition' in context)) {
throw new Error('Missing keys from context needed to sync to the URL');
}
urlStateStorage.set(
positionStateKey,
positionStateInUrlRT.encode({
position: context.latestPosition ? pickTimeKey(context.latestPosition) : null,
}),
{ replace: true }
);
};
export const initializeFromUrl =
({
positionStateKey = defaultPositionStateKey,
urlStateStorage,
toastsService,
}: LogStreamPositionUrlStateDependencies): InvokeCreator<
LogStreamPositionContext,
LogStreamPositionEvent
> =>
(_context, _event) =>
(send) => {
const positionQueryValueFromUrl = urlStateStorage.get(positionStateKey) ?? {};
const initialUrlValues = pipe(
decodePositionQueryValueFromUrl(positionQueryValueFromUrl),
Either.map(({ position }) => ({
targetPosition: position?.time
? {
time: position.time,
tiebreaker: position.tiebreaker ?? 0,
}
: null,
})),
Either.map(({ targetPosition }) => ({
targetPosition,
latestPosition: targetPosition,
}))
);
if (Either.isLeft(initialUrlValues)) {
withNotifyOnErrors(toastsService).onGetError(
createPlainError(formatErrors(initialUrlValues.left))
);
send({
type: 'INITIALIZED_FROM_URL',
targetPosition: null,
latestPosition: null,
});
} else {
send({
type: 'INITIALIZED_FROM_URL',
targetPosition: initialUrlValues.right.targetPosition ?? null,
latestPosition: initialUrlValues.right.latestPosition ?? null,
});
}
};
export const positionStateInUrlRT = rt.partial({
position: rt.union([rt.partial(minimalTimeKeyRT.props), rt.null]),
});
export type PositionStateInUrl = rt.TypeOf<typeof positionStateInUrlRT>;
const decodePositionQueryValueFromUrl = (queryValueFromUrl: unknown) => {
return positionStateInUrlRT.decode(queryValueFromUrl);
};

View file

@ -1,8 +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 './src';

View file

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const DEFAULT_QUERY = {
language: 'kuery',
query: '',
};
export const DEFAULT_FILTERS = [];
export const DEFAULT_TIMERANGE = {
from: 'now-1d',
to: 'now',
};
export const DEFAULT_REFRESH_TIME_RANGE = DEFAULT_TIMERANGE;

View file

@ -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.
*/
/* eslint-disable max-classes-per-file */
export class UnsupportedLanguageError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class QueryParsingError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}

View file

@ -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.
*/
export * from './errors';
export * from './state_machine';
export * from './types';
export * from './url_state_storage_service';
export * from './time_filter_state_service';
export * from './defaults';

View file

@ -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 type { RefreshInterval } from '@kbn/data-plugin/public';
import type { ExtendedTimeRange, LogStreamQueryContext, ParsedQuery, Timestamps } from './types';
export interface TimeChangedEvent {
type: 'TIME_CHANGED';
timeRange: ExtendedTimeRange;
refreshInterval: RefreshInterval;
timestamps: Timestamps;
}
export type LogStreamQueryNotificationEvent =
| {
type: 'VALID_QUERY_CHANGED';
parsedQuery: ParsedQuery;
}
| {
type: 'INVALID_QUERY_CHANGED';
parsedQuery: ParsedQuery;
error: Error;
}
| TimeChangedEvent;
export const logStreamQueryNotificationEventSelectors = {
validQueryChanged: (context: LogStreamQueryContext) =>
'parsedQuery' in context
? ({
type: 'VALID_QUERY_CHANGED',
parsedQuery: context.parsedQuery,
} as LogStreamQueryNotificationEvent)
: undefined,
invalidQueryChanged: (context: LogStreamQueryContext) =>
'validationError' in context
? ({
type: 'INVALID_QUERY_CHANGED',
parsedQuery: context.parsedQuery,
error: context.validationError,
} as LogStreamQueryNotificationEvent)
: undefined,
timeChanged: (context: LogStreamQueryContext) => {
return 'timeRange' in context && 'refreshInterval' in context && 'timestamps' in context
? ({
type: 'TIME_CHANGED',
timeRange: context.timeRange,
refreshInterval: context.refreshInterval,
timestamps: context.timestamps,
} as LogStreamQueryNotificationEvent)
: undefined;
},
};

View file

@ -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 type { FilterManager, QueryStringContract } from '@kbn/data-plugin/public';
import { map } from 'rxjs';
import type { InvokeCreator } from 'xstate';
import type { LogStreamQueryContext, LogStreamQueryEvent } from './types';
export const subscribeToQuerySearchBarChanges =
({
queryStringService,
}: {
queryStringService: QueryStringContract;
}): InvokeCreator<LogStreamQueryContext, LogStreamQueryEvent> =>
(context) =>
queryStringService.getUpdates$().pipe(
map(() => queryStringService.getQuery()),
map((query): LogStreamQueryEvent => {
return {
type: 'QUERY_FROM_SEARCH_BAR_CHANGED',
query,
};
})
);
export const updateQueryInSearchBar =
({ queryStringService }: { queryStringService: QueryStringContract }) =>
(context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
if ('query' in context) {
queryStringService.setQuery(context.query);
}
};
export const subscribeToFilterSearchBarChanges =
({
filterManagerService,
}: {
filterManagerService: FilterManager;
}): InvokeCreator<LogStreamQueryContext, LogStreamQueryEvent> =>
(context) =>
filterManagerService.getUpdates$().pipe(
map(() => filterManagerService.getFilters()),
map((filters): LogStreamQueryEvent => {
return {
type: 'FILTERS_FROM_SEARCH_BAR_CHANGED',
filters,
};
})
);
export const updateFiltersInSearchBar =
({ filterManagerService }: { filterManagerService: FilterManager }) =>
(context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
if ('filters' in context) {
filterManagerService.setFilters(context.filters);
}
};

View file

@ -1,362 +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 { IToasts } from '@kbn/core-notifications-browser';
import type {
FilterManager,
QueryStringContract,
TimefilterContract,
} from '@kbn/data-plugin/public';
import type { EsQueryConfig } from '@kbn/es-query';
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import type { ActorRefFrom } from 'xstate';
import { actions, createMachine, SpecialTargets, send } from 'xstate';
import { DEFAULT_REFRESH_INTERVAL } from '@kbn/logs-shared-plugin/common';
import type { OmitDeprecatedState } from '@kbn/xstate-utils';
import { sendIfDefined } from '@kbn/xstate-utils';
import { logStreamQueryNotificationEventSelectors } from './notifications';
import {
subscribeToFilterSearchBarChanges,
subscribeToQuerySearchBarChanges,
updateFiltersInSearchBar,
updateQueryInSearchBar,
} from './search_bar_state_service';
import type {
LogStreamQueryContext,
LogStreamQueryContextWithDataViews,
LogStreamQueryContextWithFilters,
LogStreamQueryContextWithParsedQuery,
LogStreamQueryContextWithQuery,
LogStreamQueryContextWithRefreshInterval,
LogStreamQueryContextWithTime,
LogStreamQueryContextWithTimeRange,
LogStreamQueryContextWithValidationError,
LogStreamQueryEvent,
LogStreamQueryTypestate,
} from './types';
import {
initializeFromUrl,
safeDefaultParsedQuery,
updateContextInUrl,
} from './url_state_storage_service';
import {
initializeFromTimeFilterService,
subscribeToTimeFilterServiceChanges,
updateTimeContextFromTimeFilterService,
updateTimeContextFromTimeRangeUpdate,
updateTimeContextFromRefreshIntervalUpdate,
updateTimeInTimeFilterService,
updateTimeContextFromUrl,
} from './time_filter_state_service';
import { showValidationErrorToast, validateQuery } from './validate_query_service';
import { DEFAULT_REFRESH_TIME_RANGE } from './defaults';
export const createPureLogStreamQueryStateMachine = (
initialContext: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime
) =>
/** @xstate-layout N4IgpgJg5mDOIC5QEUCuYBOBPAdKgdgJZEAuhAhgDaEBekAxANoAMAuoqAA4D2shZ3fBxAAPRAEYAHAGYck8QCYArAoAsC8QHZNygJySANCCwTpszc2njpANnH2FN1UtUBfV0bSZcxfhWo0xFAAYhjcALYAqhiU9ACSAHJxACpxAIIAMnEAWgCiACIA+sEASgDyALKFkSUZLOxIIDx8AkKNYghKzJJyCvJOurqqA4bGiPI4XczTdjbM2kq64u6e6Ng4vmRUtEGhEcmE4WDBhJQkmADKmABuhADGYPFJqZk5BcXlVakVucVxGclciVChcgQA1OIAYVy9WEzT8gmEHWkkhsOGmmnkqnUumkzBsmmkRhMCF0mjRunxCjM9nx0iUkhWIC8602-lokBwAEc1lh6MhIkCAJofSog3JpEqQgAShQAQpLCjK0gkAOIFWGNeGtJHjcRKHA2aR9BT45hKOxKIljBDiZiqcRyAmmySqV2aJSYpksnykdl0CDc3n0YL-QElC6iqqgyUy+WK5VqjVsOG8BFtUAdeQGo0ms0W-XWklLHrDZi6K3SbQ2SSe728jZ+7YBoPeej5NLJNKFCG5ADqkcT6vymq4aZ17T1OeNklNcwLVuJ4wUCnR83kgzd7vr3kbfmbnJ5u+u2wg5DI+Cg9DBrw7qTKCRBkUh0IKyYaY5ahERk9tDtUODSJSdj6uW4i6EupKqMwgGKEoijMCaZKMh4zINmyB6Bke6wntQZ4XleN5ZHecQPsUaT-O+qZfj+mYSP+gHAfYXRLBBNr2HMOBaOBfQbto5o7qyTYBIeDYYGAuGEPhQTXrenakY+wQURkVFauO34ZqI9HiABQH4sxYFsSSdq2Dg5IWoSmiKDY+jSIJvr7iJWFiRJp7njJRFxCRZEXM+r75Kpn7prqf46Yx+mgaxkH9DgwHKDZGgrha9l7lsTk4GQRz0N8vylGKOV-ACQLiiUELQkq0oqsOo5NOptFaQg2aGjOc7mpaRYSO6hoWho5YKBYHooasu4YelmWPJEAAKd6-AVJRVTCKZqTRmlZvqzV5vO7WQeIBIwdIqhVjWZLDHMNgpaNHKBuN9BTTNhQlLkpS5BcsqJOGRE1dqGkhU1uazvm23saouLolYxoWCuahKEoF3CVdGWHGAqX+gwX11atpgOpM1KnYdmIVqoO06LokwyGYh2SJo2KqJocOOQj40o5hTDiB+tUrSF1jYyoB02E4VaSITxOSD03Q0tTQFujo9NpYzSM4LAJDuXc9CTWk6qFLkCRFHKkTBMExWPWkMqBRzwW-rtFjosMQv0rSpqaDtVMAXauKYrO8HMDpsuo9dCtK+J5DhDJIhK+eyPkAAZucGAABTiVH4mwAAFgAlPy6Hwy2TOB2AwdBOjnOW4SpNC+6qICxuxMooBZgQ+aB2WO4qH4NwEBwMIPrURbdEIAAtDYkH9wagxj97tjDCuVZuKhPp4EQ2eQD3E59+oO3GmZli7dYnoMn0dNz1nDOBJeexRDEK8-b+-U9OXwyWVoLFE+x5Y4KaVjUkaihC7TvvNrsMI4QDhHBOGcS4Nx7hgCvvVDoBJIKDH2osKsVYKzgUGP-JyMDMYICsqTf6rUFwdQQCDGCcUpC6BNPIOyR8RpL2ct4bBIVySaA2gDLahZIKelLFSU0iE5hkkwQjbCuBJLSUvEwy2UhWF9EUMoLQbpJ4IJcJMICdpdrQ12kNNCdCT6iWPKeSRfddr80NBWPEQE5jYhsAoaKzgcDYkcKLZx28aHDSEnohhQkxFGIaiYtENkrSWF0FYqeO18QGjxBoQ69J6Solnu4hycsWwiJwOJMR7kJHLV7n46R795BJQUTIJwdiAICKAsaA+kh7RCJzkjXxHQWFsMIUDYyeI0RWmsALBCNZYa0I8ckzkTNLoBgaRIDQrCyQ2AZPpPhSga4ATUIMV0CgyT4nOv0pJftEZHEVsrMgdwxm2ngq7cxVp+aU3AiLNE9cG5Wmgm4nRAztm5xIEHEOWSgqrz8ScriZzbBVyuexHQrDyZWE9M4TQ+hD7uCAA */
createMachine<LogStreamQueryContext, LogStreamQueryEvent, LogStreamQueryTypestate>(
{
context: initialContext,
preserveActionOrder: true,
predictableActionArguments: true,
id: 'Query',
initial: 'uninitialized',
states: {
uninitialized: {
always: {
target: 'initializingFromTimeFilterService',
},
},
initializingFromUrl: {
on: {
INITIALIZED_FROM_URL: {
target: 'initialized',
actions: ['storeQuery', 'storeFilters', 'updateTimeContextFromUrl'],
},
},
invoke: {
src: 'initializeFromUrl',
},
},
initializingFromTimeFilterService: {
on: {
INITIALIZED_FROM_TIME_FILTER_SERVICE: {
target: 'initializingFromUrl',
actions: ['updateTimeContextFromTimeFilterService'],
},
},
invoke: {
src: 'initializeFromTimeFilterService',
},
},
initialized: {
type: 'parallel',
states: {
query: {
entry: ['updateContextInUrl', 'updateQueryInSearchBar', 'updateFiltersInSearchBar'],
invoke: [
{
src: 'subscribeToQuerySearchBarChanges',
},
{
src: 'subscribeToFilterSearchBarChanges',
},
],
initial: 'validating',
states: {
validating: {
invoke: {
src: 'validateQuery',
},
on: {
VALIDATION_SUCCEEDED: {
target: 'valid',
actions: 'storeParsedQuery',
},
VALIDATION_FAILED: {
target: 'invalid',
actions: [
'storeValidationError',
'storeDefaultParsedQuery',
'showValidationErrorToast',
],
},
},
},
valid: {
entry: 'notifyValidQueryChanged',
},
invalid: {
entry: 'notifyInvalidQueryChanged',
},
revalidating: {
invoke: {
src: 'validateQuery',
},
on: {
VALIDATION_FAILED: {
target: 'invalid',
actions: ['storeValidationError', 'showValidationErrorToast'],
},
VALIDATION_SUCCEEDED: {
target: 'valid',
actions: ['clearValidationError', 'storeParsedQuery'],
},
},
},
},
on: {
QUERY_FROM_SEARCH_BAR_CHANGED: {
target: '.revalidating',
actions: ['storeQuery', 'updateContextInUrl'],
},
FILTERS_FROM_SEARCH_BAR_CHANGED: {
target: '.revalidating',
actions: ['storeFilters', 'updateContextInUrl'],
},
DATA_VIEWS_CHANGED: {
target: '.revalidating',
actions: 'storeDataViews',
},
},
},
time: {
initial: 'initialized',
entry: ['notifyTimeChanged', 'updateTimeInTimeFilterService'],
invoke: [
{
src: 'subscribeToTimeFilterServiceChanges',
},
],
states: {
initialized: {
always: [{ target: 'streaming', cond: 'isStreaming' }, { target: 'static' }],
},
static: {
on: {
PAGE_END_BUFFER_REACHED: {
actions: ['expandPageEnd'],
},
},
},
streaming: {
after: {
refresh: { target: 'streaming', actions: ['refreshTime'] },
},
},
},
on: {
TIME_FROM_TIME_FILTER_SERVICE_CHANGED: {
target: '.initialized',
actions: [
'updateTimeContextFromTimeFilterService',
'notifyTimeChanged',
'updateContextInUrl',
],
},
UPDATE_TIME_RANGE: {
target: '.initialized',
actions: [
'updateTimeContextFromTimeRangeUpdate',
'notifyTimeChanged',
'updateTimeInTimeFilterService',
'updateContextInUrl',
],
},
UPDATE_REFRESH_INTERVAL: {
target: '.initialized',
actions: [
'updateTimeContextFromRefreshIntervalUpdate',
'notifyTimeChanged',
'updateTimeInTimeFilterService',
'updateContextInUrl',
],
},
},
},
},
},
},
},
{
actions: {
notifyInvalidQueryChanged: actions.pure(() => undefined),
notifyValidQueryChanged: actions.pure(() => undefined),
notifyTimeChanged: actions.pure(() => undefined),
storeQuery: actions.assign((_context, event) => {
return 'query' in event ? ({ query: event.query } as LogStreamQueryContextWithQuery) : {};
}),
storeFilters: actions.assign((_context, event) =>
'filters' in event ? ({ filters: event.filters } as LogStreamQueryContextWithFilters) : {}
),
storeTimeRange: actions.assign((_context, event) =>
'timeRange' in event
? ({ timeRange: event.timeRange } as LogStreamQueryContextWithTimeRange)
: {}
),
storeRefreshInterval: actions.assign((_context, event) =>
'refreshInterval' in event
? ({
refreshInterval: event.refreshInterval,
} as LogStreamQueryContextWithRefreshInterval)
: {}
),
storeDataViews: actions.assign((_context, event) =>
'dataViews' in event
? ({ dataViews: event.dataViews } as LogStreamQueryContextWithDataViews)
: {}
),
storeValidationError: actions.assign((_context, event) =>
'error' in event
? ({
validationError: event.error,
} as LogStreamQueryContextWithQuery & LogStreamQueryContextWithValidationError)
: {}
),
storeDefaultParsedQuery: actions.assign(
(_context, _event) =>
({ parsedQuery: safeDefaultParsedQuery } as LogStreamQueryContextWithParsedQuery)
),
storeParsedQuery: actions.assign((_context, event) =>
'parsedQuery' in event
? ({ parsedQuery: event.parsedQuery } as LogStreamQueryContextWithParsedQuery)
: {}
),
clearValidationError: actions.assign(
(_context, _event) =>
({ validationError: undefined } as Omit<
LogStreamQueryContextWithValidationError,
'validationError'
>)
),
updateTimeContextFromTimeFilterService,
updateTimeContextFromTimeRangeUpdate,
updateTimeContextFromRefreshIntervalUpdate,
refreshTime: send({ type: 'UPDATE_TIME_RANGE', timeRange: DEFAULT_REFRESH_TIME_RANGE }),
expandPageEnd: send((context) => ({
type: 'UPDATE_TIME_RANGE',
timeRange: { to: context.timeRange.to },
})),
updateTimeContextFromUrl,
},
guards: {
isStreaming: (context, event) =>
'refreshInterval' in context ? !context.refreshInterval.pause : false,
},
delays: {
refresh: (context, event) =>
'refreshInterval' in context
? context.refreshInterval.value
: DEFAULT_REFRESH_INTERVAL.value,
},
}
);
export interface LogStreamQueryStateMachineDependencies {
kibanaQuerySettings: EsQueryConfig;
queryStringService: QueryStringContract;
filterManagerService: FilterManager;
urlStateStorage: IKbnUrlStateStorage;
toastsService: IToasts;
timeFilterService: TimefilterContract;
}
export const createLogStreamQueryStateMachine = (
initialContext: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime,
{
kibanaQuerySettings,
queryStringService,
toastsService,
filterManagerService,
urlStateStorage,
timeFilterService,
}: LogStreamQueryStateMachineDependencies
) =>
createPureLogStreamQueryStateMachine(initialContext).withConfig({
actions: {
updateContextInUrl: updateContextInUrl({ toastsService, urlStateStorage }),
// Query
notifyInvalidQueryChanged: sendIfDefined(SpecialTargets.Parent)(
logStreamQueryNotificationEventSelectors.invalidQueryChanged
),
notifyValidQueryChanged: sendIfDefined(SpecialTargets.Parent)(
logStreamQueryNotificationEventSelectors.validQueryChanged
),
showValidationErrorToast: showValidationErrorToast({ toastsService }),
updateQueryInSearchBar: updateQueryInSearchBar({ queryStringService }),
updateFiltersInSearchBar: updateFiltersInSearchBar({ filterManagerService }),
// Time
updateTimeInTimeFilterService: updateTimeInTimeFilterService({ timeFilterService }),
notifyTimeChanged: sendIfDefined(SpecialTargets.Parent)(
logStreamQueryNotificationEventSelectors.timeChanged
),
},
services: {
initializeFromUrl: initializeFromUrl({ toastsService, urlStateStorage }),
initializeFromTimeFilterService: initializeFromTimeFilterService({ timeFilterService }),
validateQuery: validateQuery({ kibanaQuerySettings }),
subscribeToQuerySearchBarChanges: subscribeToQuerySearchBarChanges({
queryStringService,
}),
subscribeToFilterSearchBarChanges: subscribeToFilterSearchBarChanges({
filterManagerService,
}),
subscribeToTimeFilterServiceChanges: subscribeToTimeFilterServiceChanges({
timeFilterService,
}),
},
});
export type LogStreamQueryStateMachine = ReturnType<typeof createLogStreamQueryStateMachine>;
export type LogStreamQueryActorRef = OmitDeprecatedState<ActorRefFrom<LogStreamQueryStateMachine>>;

View file

@ -1,192 +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 { RefreshInterval, TimefilterContract } from '@kbn/data-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { map, merge } from 'rxjs';
import type { InvokeCreator } from 'xstate';
import { actions } from 'xstate';
import { datemathToEpochMillis } from '../../../utils/datemath';
import { DEFAULT_REFRESH_TIME_RANGE } from './defaults';
import type { LogStreamQueryContext, LogStreamQueryEvent } from './types';
export interface TimefilterState {
timeRange: TimeRange;
refreshInterval: RefreshInterval;
}
export const initializeFromTimeFilterService =
({
timeFilterService,
}: {
timeFilterService: TimefilterContract;
}): InvokeCreator<LogStreamQueryContext, LogStreamQueryEvent> =>
(_context, _event) =>
(send) => {
const timeRange = timeFilterService.getTime();
const refreshInterval = timeFilterService.getRefreshInterval();
send({
type: 'INITIALIZED_FROM_TIME_FILTER_SERVICE',
timeRange,
refreshInterval,
});
};
export const updateTimeInTimeFilterService =
({ timeFilterService }: { timeFilterService: TimefilterContract }) =>
(context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
if ('timeRange' in context) {
timeFilterService.setTime(context.timeRange);
}
if ('refreshInterval' in context) {
timeFilterService.setRefreshInterval(context.refreshInterval);
}
};
export const subscribeToTimeFilterServiceChanges =
({
timeFilterService,
}: {
timeFilterService: TimefilterContract;
}): InvokeCreator<LogStreamQueryContext, LogStreamQueryEvent> =>
(context) =>
merge(timeFilterService.getTimeUpdate$(), timeFilterService.getRefreshIntervalUpdate$()).pipe(
map(() => getTimefilterState(timeFilterService)),
map((timeState): LogStreamQueryEvent => {
return {
type: 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED',
...timeState,
};
})
);
const getTimefilterState = (timeFilterService: TimefilterContract): TimefilterState => ({
timeRange: timeFilterService.getTime(),
refreshInterval: timeFilterService.getRefreshInterval(),
});
export const updateTimeContextFromTimeFilterService = actions.assign(
(context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
if (
event.type === 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED' ||
event.type === 'INITIALIZED_FROM_TIME_FILTER_SERVICE'
) {
return {
...getTimeFromEvent(context, event),
refreshInterval:
event.type === 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED'
? event.refreshInterval
: { ...context.refreshInterval, pause: event.refreshInterval.pause },
};
} else {
return {};
}
}
);
export const updateTimeContextFromUrl = actions.assign(
(context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
if (event.type === 'INITIALIZED_FROM_URL') {
return {
...('timeRange' in event && event.timeRange ? { ...getTimeFromEvent(context, event) } : {}),
...('refreshInterval' in event && event.refreshInterval
? { refreshInterval: event.refreshInterval }
: {}),
};
} else {
return {};
}
}
);
export const updateTimeContextFromTimeRangeUpdate = actions.assign(
(context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
if ('timeRange' in event && event.type === 'UPDATE_TIME_RANGE') {
return getTimeFromEvent(context, event);
} else {
return {};
}
}
);
export const updateTimeContextFromRefreshIntervalUpdate = actions.assign(
(context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
if (
'refreshInterval' in event &&
'refreshInterval' in context &&
event.type === 'UPDATE_REFRESH_INTERVAL'
) {
const pause = event.refreshInterval.pause ?? context.refreshInterval.pause;
const value = event.refreshInterval.value ?? context.refreshInterval.value;
const nowTimestamp = Date.now();
const draftContext = {
refreshInterval: {
pause,
value,
},
...(!pause
? {
timeRange: {
...DEFAULT_REFRESH_TIME_RANGE,
lastChangedCompletely: nowTimestamp,
},
}
: {}),
...(!pause
? {
timestamps: {
startTimestamp: datemathToEpochMillis(DEFAULT_REFRESH_TIME_RANGE.from, 'down') ?? 0,
endTimestamp: datemathToEpochMillis(DEFAULT_REFRESH_TIME_RANGE.to, 'up') ?? 0,
lastChangedTimestamp: nowTimestamp,
},
}
: {}),
};
return draftContext;
} else {
return {};
}
}
);
const getTimeFromEvent = (context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
if (!('timeRange' in event) || !('timeRange' in context) || !('timestamps' in context)) {
throw new Error('Missing keys to get time from event');
}
const nowTimestamp = Date.now();
const from = event.timeRange?.from ?? context.timeRange.from;
const to = event.timeRange?.to ?? context.timeRange.to;
const fromTimestamp = event.timeRange?.from
? datemathToEpochMillis(from, 'down')
: context.timestamps.startTimestamp;
const toTimestamp = event.timeRange?.to
? datemathToEpochMillis(to, 'up')
: context.timestamps.endTimestamp;
return {
timeRange: {
from,
to,
lastChangedCompletely:
event.timeRange?.from && event.timeRange?.to
? nowTimestamp
: context.timeRange.lastChangedCompletely,
},
timestamps: {
startTimestamp: fromTimestamp ?? 0,
endTimestamp: toTimestamp ?? 0,
lastChangedTimestamp: nowTimestamp,
},
};
};

View file

@ -1,160 +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 { RefreshInterval } from '@kbn/data-plugin/public';
import type {
AggregateQuery,
BoolQuery,
DataViewBase,
Query,
Filter,
TimeRange,
} from '@kbn/es-query';
import type { PageEndBufferReachedEvent } from '../../log_stream_position_state/src/notifications';
export type AnyQuery = Query | AggregateQuery;
export interface ParsedQuery {
bool: BoolQuery;
}
export interface LogStreamQueryContextWithDataViews {
dataViews: DataViewBase[];
}
export interface LogStreamQueryContextWithSavedQueryId {
savedQueryId: string;
}
export interface LogStreamQueryContextWithQuery {
query: AnyQuery;
}
export interface LogStreamQueryContextWithParsedQuery {
parsedQuery: ParsedQuery;
}
export interface LogStreamQueryContextWithFilters {
filters: Filter[];
}
export interface LogStreamQueryContextWithValidationError {
validationError: Error;
}
export type ExtendedTimeRange = TimeRange & { lastChangedCompletely: number };
export interface LogStreamQueryContextWithTimeRange {
timeRange: ExtendedTimeRange;
}
export interface LogStreamQueryContextWithRefreshInterval {
refreshInterval: RefreshInterval;
}
export interface Timestamps {
startTimestamp: number;
endTimestamp: number;
lastChangedTimestamp: number;
}
export interface LogStreamQueryContextWithTimestamps {
timestamps: Timestamps;
}
export type LogStreamQueryContextWithTime = LogStreamQueryContextWithTimeRange &
LogStreamQueryContextWithRefreshInterval &
LogStreamQueryContextWithTimestamps;
export type LogStreamQueryTypestate =
| {
value: 'uninitialized';
context: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime;
}
| {
value: 'query' | { query: 'validating' };
context: LogStreamQueryContextWithDataViews &
LogStreamQueryContextWithParsedQuery &
LogStreamQueryContextWithQuery &
LogStreamQueryContextWithFilters &
LogStreamQueryContextWithTime;
}
| {
value: { query: 'valid' };
context: LogStreamQueryContextWithDataViews &
LogStreamQueryContextWithParsedQuery &
LogStreamQueryContextWithQuery &
LogStreamQueryContextWithFilters &
LogStreamQueryContextWithTime;
}
| {
value: { query: 'invalid' };
context: LogStreamQueryContextWithDataViews &
LogStreamQueryContextWithParsedQuery &
LogStreamQueryContextWithQuery &
LogStreamQueryContextWithFilters &
LogStreamQueryContextWithTime &
LogStreamQueryContextWithValidationError;
}
| {
value: 'time' | { time: 'initialized' } | { time: 'streaming' } | { time: 'static' };
context: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime;
};
export type LogStreamQueryContext = LogStreamQueryTypestate['context'];
export type LogStreamQueryStateValue = LogStreamQueryTypestate['value'];
export interface UpdateTimeRangeEvent {
type: 'UPDATE_TIME_RANGE';
timeRange: Partial<TimeRange>;
}
export interface UpdateRefreshIntervalEvent {
type: 'UPDATE_REFRESH_INTERVAL';
refreshInterval: Partial<RefreshInterval>;
}
export type LogStreamQueryEvent =
| {
type: 'QUERY_FROM_SEARCH_BAR_CHANGED';
query: AnyQuery;
}
| {
type: 'FILTERS_FROM_SEARCH_BAR_CHANGED';
filters: Filter[];
}
| {
type: 'DATA_VIEWS_CHANGED';
dataViews: DataViewBase[];
}
| {
type: 'VALIDATION_SUCCEEDED';
parsedQuery: ParsedQuery;
}
| {
type: 'VALIDATION_FAILED';
error: Error;
}
| {
type: 'INITIALIZED_FROM_URL';
query: AnyQuery;
filters: Filter[];
timeRange: TimeRange | null;
refreshInterval: RefreshInterval | null;
}
| {
type: 'INITIALIZED_FROM_TIME_FILTER_SERVICE';
timeRange: TimeRange;
refreshInterval: RefreshInterval;
}
| {
type: 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED';
timeRange: TimeRange;
refreshInterval: RefreshInterval;
}
| UpdateTimeRangeEvent
| UpdateRefreshIntervalEvent
| PageEndBufferReachedEvent;

View file

@ -1,284 +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 { IToasts } from '@kbn/core-notifications-browser';
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
import * as Array from 'fp-ts/lib/Array';
import * as Either from 'fp-ts/lib/Either';
import { identity, pipe } from 'fp-ts/lib/function';
import * as rt from 'io-ts';
import type { InvokeCreator } from 'xstate';
import {
defaultFilterStateKey,
defaultPositionStateKey,
DEFAULT_REFRESH_INTERVAL,
} from '@kbn/logs-shared-plugin/common';
import moment from 'moment';
import { createPlainError, formatErrors } from '@kbn/io-ts-utils';
import {
getTimeRangeEndFromTime,
getTimeRangeStartFromTime,
} from '../../../../common/url_state_storage_service';
import { minimalTimeKeyRT } from '../../../../common/time';
import { datemathStringRT } from '../../../utils/datemath';
import type { LogStreamQueryContext, LogStreamQueryEvent, ParsedQuery } from './types';
import { DEFAULT_FILTERS, DEFAULT_QUERY, DEFAULT_TIMERANGE } from './defaults';
interface LogStreamQueryUrlStateDependencies {
filterStateKey?: string;
positionStateKey?: string;
savedQueryIdKey?: string;
toastsService: IToasts;
urlStateStorage: IKbnUrlStateStorage;
}
type RequiredDefaults = Required<Omit<FilterStateInUrl, 'timeRange' | 'refreshInterval'>>;
type OptionalDefaults = Pick<FilterStateInUrl, 'timeRange' | 'refreshInterval'>;
type FullDefaults = Required<RequiredDefaults & OptionalDefaults>;
const requiredDefaultFilterStateValue: RequiredDefaults = {
query: DEFAULT_QUERY,
filters: DEFAULT_FILTERS,
};
const optionalDefaultFilterStateValue = {
timeRange: DEFAULT_TIMERANGE,
refreshInterval: DEFAULT_REFRESH_INTERVAL,
};
const defaultFilterStateValue: FullDefaults = {
...requiredDefaultFilterStateValue,
...optionalDefaultFilterStateValue,
};
export const safeDefaultParsedQuery: ParsedQuery = {
bool: {
must: [],
must_not: [],
should: [],
filter: [{ match_none: {} }],
},
};
export const updateContextInUrl =
({
urlStateStorage,
filterStateKey = defaultFilterStateKey,
}: LogStreamQueryUrlStateDependencies) =>
(context: LogStreamQueryContext, _event: LogStreamQueryEvent) => {
if (
!('query' in context) ||
!('filters' in context) ||
!('timeRange' in context) ||
!('refreshInterval' in context)
) {
throw new Error('Missing keys from context needed to sync to the URL');
}
urlStateStorage.set(
filterStateKey,
filterStateInUrlRT.encode({
query: context.query,
filters: context.filters,
timeRange: context.timeRange,
refreshInterval: context.refreshInterval,
}),
{ replace: true }
);
};
export const initializeFromUrl =
({
filterStateKey = defaultFilterStateKey,
positionStateKey = defaultPositionStateKey,
toastsService,
urlStateStorage,
}: LogStreamQueryUrlStateDependencies): InvokeCreator<
LogStreamQueryContext,
LogStreamQueryEvent
> =>
(_context, _event) =>
(send) => {
const filterQueryValueFromUrl =
urlStateStorage.get(filterStateKey) ?? requiredDefaultFilterStateValue;
const filterQueryE = decodeFilterQueryValueFromUrl(filterQueryValueFromUrl);
// NOTE: Access logPosition for backwards compatibility with values previously stored under that key.
const positionQueryValueFromUrl = urlStateStorage.get(positionStateKey) ?? {};
const positionQueryE = decodePositionQueryValueFromUrl(positionQueryValueFromUrl);
if (Either.isLeft(filterQueryE) || Either.isLeft(positionQueryE)) {
withNotifyOnErrors(toastsService).onGetError(
createPlainError(
formatErrors([
...(Either.isLeft(filterQueryE) ? filterQueryE.left : []),
...(Either.isLeft(positionQueryE) ? positionQueryE.left : []),
])
)
);
send({
type: 'INITIALIZED_FROM_URL',
query: defaultFilterStateValue.query,
filters: defaultFilterStateValue.filters,
timeRange: null,
refreshInterval: null,
});
} else {
send({
type: 'INITIALIZED_FROM_URL',
query: filterQueryE.right.query ?? defaultFilterStateValue.query,
filters: filterQueryE.right.filters ?? defaultFilterStateValue.filters,
timeRange: pipe(
// Via the logFilter key
pipe(
filterQueryE.right.timeRange,
Either.fromNullable(null),
Either.chain(({ from, to }) =>
from && to ? Either.right({ from, to }) : Either.left(null)
)
),
// Via the legacy logPosition key, and start / end timeRange parameters
Either.alt(() =>
pipe(
positionQueryE.right,
Either.fromNullable(null),
Either.chain(({ start, end }) =>
start && end ? Either.right({ from: start, to: end }) : Either.left(null)
)
)
),
// Via the legacy logPosition key, and deriving from / to from position.time
Either.alt(() =>
pipe(
positionQueryE.right,
Either.fromNullable(null),
Either.chain(({ position }) =>
position && position.time
? Either.right({
from: getTimeRangeStartFromTime(moment(position.time).valueOf()),
to: getTimeRangeEndFromTime(moment(position.time).valueOf()),
})
: Either.left(null)
)
)
),
Either.fold(identity, identity)
),
refreshInterval: pipe(
// Via the logFilter key
pipe(filterQueryE.right.refreshInterval, Either.fromNullable(null)),
// Via the legacy logPosition key, and the boolean streamLive parameter
Either.alt(() =>
pipe(
positionQueryE.right,
Either.fromNullable(null),
Either.chain(({ streamLive }) =>
typeof streamLive === 'boolean'
? Either.right({
pause: !streamLive,
value: defaultFilterStateValue.refreshInterval.value, // NOTE: Was not previously synced to the URL, so falls straight to the default.
})
: Either.left(null)
)
)
),
Either.fold(identity, identity)
),
});
}
};
const legacyLegacyFilterStateWithExpressionInUrlRT = rt.type({
kind: rt.literal('kuery'),
expression: rt.string,
});
export const legacyPositionStateInUrlRT = rt.partial({
streamLive: rt.boolean,
start: datemathStringRT,
end: datemathStringRT,
position: rt.union([rt.partial(minimalTimeKeyRT.props), rt.null]),
});
const decodeFilterQueryValueFromUrl = (queryValueFromUrl: unknown) =>
Either.getAltValidation(Array.getMonoid<rt.ValidationError>()).alt<FilterStateInUrl>(
pipe(
pipe(
legacyLegacyFilterStateWithExpressionInUrlRT.decode(queryValueFromUrl),
Either.map(({ expression, kind }) => ({ query: { language: kind, query: expression } }))
),
Either.alt(() =>
pipe(
legacyFilterStateInUrlRT.decode(queryValueFromUrl),
Either.map((legacyQuery) => ({ query: legacyQuery }))
)
)
),
() => filterStateInUrlRT.decode(queryValueFromUrl)
);
const decodePositionQueryValueFromUrl = (queryValueFromUrl: unknown) => {
return legacyPositionStateInUrlRT.decode(queryValueFromUrl);
};
export type FilterStateInUrl = rt.TypeOf<typeof filterStateInUrlRT>;
export const filterMeta = rt.partial({
alias: rt.union([rt.string, rt.null]),
disabled: rt.boolean,
negate: rt.boolean,
controlledBy: rt.string,
group: rt.string,
index: rt.string,
isMultiIndex: rt.boolean,
type: rt.string,
key: rt.string,
params: rt.any,
value: rt.any,
});
export const filter = rt.intersection([
rt.type({
meta: filterMeta,
}),
rt.partial({
query: rt.UnknownRecord,
}),
]);
export const filterStateInUrlRT = rt.partial({
query: rt.union([
rt.strict({
language: rt.string,
query: rt.union([rt.string, rt.record(rt.string, rt.unknown)]),
}),
rt.strict({
esql: rt.string,
}),
]),
filters: rt.array(filter),
timeRange: rt.strict({
from: rt.string,
to: rt.string,
}),
refreshInterval: rt.strict({
pause: rt.boolean,
value: rt.number,
}),
});
export const legacyFilterStateInUrlRT = rt.union([
rt.strict({
language: rt.string,
query: rt.union([rt.string, rt.record(rt.string, rt.unknown)]),
}),
rt.strict({
esql: rt.string,
}),
]);

View file

@ -1,71 +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 { IToasts } from '@kbn/core-notifications-browser';
import type { EsQueryConfig } from '@kbn/es-query';
import { buildEsQuery, isOfQueryType } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import type { InvokeCreator } from 'xstate';
import { QueryParsingError, UnsupportedLanguageError } from './errors';
import type { LogStreamQueryContext, LogStreamQueryEvent } from './types';
export const validateQuery =
({
kibanaQuerySettings,
}: {
kibanaQuerySettings: EsQueryConfig;
}): InvokeCreator<LogStreamQueryContext, LogStreamQueryEvent> =>
(context) =>
(send) => {
if (!('query' in context)) {
throw new Error('Failed to validate query: no query in context');
}
const { dataViews, query, filters } = context;
if (!isOfQueryType(query)) {
send({
type: 'VALIDATION_FAILED',
error: new UnsupportedLanguageError('Failed to validate query: unsupported language'),
});
return;
}
try {
const parsedQuery = buildEsQuery(dataViews, query, filters, kibanaQuerySettings);
send({
type: 'VALIDATION_SUCCEEDED',
parsedQuery,
});
} catch (error) {
send({
type: 'VALIDATION_FAILED',
error: new QueryParsingError(`${error}`),
});
}
};
export const showValidationErrorToast =
({ toastsService }: { toastsService: IToasts }) =>
(_context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
if (event.type !== 'VALIDATION_FAILED') {
return;
}
toastsService.addError(event.error, {
title: validationErrorToastTitle,
});
};
const validationErrorToastTitle = i18n.translate(
'xpack.infra.logsPage.toolbar.logFilterErrorToastTitle',
{
defaultMessage: 'Log filter error',
}
);

View file

@ -1,3 +0,0 @@
# @kbn/observability-logs-xstate-helpers
Helpers to design well-typed state machines with XState

View file

@ -1,8 +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 './src';

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './invalid_state_callout';
export * from './state_machine_playground';

View file

@ -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 { EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import stringify from 'json-stable-stringify';
import React from 'react';
import type { State } from 'xstate';
export const InvalidStateCallout: React.FC<{ state: State<any, any, any, any, any> }> = ({
state,
}) => (
<EuiCallOut title={invalidStateCalloutTitle} color="danger" iconType="warning">
<FormattedMessage
id="xpack.infra.logs.common.invalidStateMessage"
defaultMessage="Unable to handle state {stateValue}."
values={{
stateValue: stringify(state.value),
}}
tagName="pre"
/>
</EuiCallOut>
);
const invalidStateCalloutTitle = i18n.translate(
'xpack.infra.logs.common.invalidStateCalloutTitle',
{ defaultMessage: 'Invalid state encountered' }
);

View file

@ -1,81 +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 { EuiButton } from '@elastic/eui';
import React, { useCallback } from 'react';
import { useLogViewContext } from '@kbn/logs-shared-plugin/public';
export const StateMachinePlayground = () => {
const { changeLogViewReference, revertToDefaultLogView, update, isLoading, logViewStateService } =
useLogViewContext();
const switchToInlineLogView = useCallback(() => {
changeLogViewReference({
type: 'log-view-inline',
id: 'playground-log-view',
attributes: {
name: 'playground-log-view-name',
description: 'from the state machine playground',
logIndices: { type: 'index_name', indexName: 'logs-*' },
logColumns: [
{
fieldColumn: {
id: 'playground-field-column',
field: 'event.dataset',
},
},
],
},
});
}, [changeLogViewReference]);
const updateLogView = useCallback(() => {
update({
name: 'Updated playground name',
});
}, [update]);
const persistInlineLogView = useCallback(() => {
logViewStateService.send({
type: 'PERSIST_INLINE_LOG_VIEW',
});
}, [logViewStateService]);
return (
<>
{isLoading && 'Is loading'}
<EuiButton
data-test-subj="infraStateMachinePlaygroundButton"
fill
onClick={() => switchToInlineLogView()}
>
{'Switch to inline Log View'}
</EuiButton>
<EuiButton
data-test-subj="infraStateMachinePlaygroundButton"
fill
onClick={() => persistInlineLogView()}
>
{'Persist inline Log View'}
</EuiButton>
<EuiButton
data-test-subj="infraStateMachinePlaygroundButton"
fill
onClick={() => revertToDefaultLogView()}
>
{'Revert to default (persisted) Log View'}
</EuiButton>
<EuiButton
data-test-subj="infraStateMachinePlaygroundButton"
fill
onClick={() => updateLogView()}
>
{'Update log view'}
</EuiButton>
</>
);
};

View file

@ -115,8 +115,8 @@ export const CategoryExampleMessage: React.FunctionComponent<{
onClose={closeMenu}
items={[
{
label: i18n.translate('xpack.infra.logs.categoryExample.viewInStreamText', {
defaultMessage: 'View in stream',
label: i18n.translate('xpack.infra.logs.categoryExample.viewInDiscoverText', {
defaultMessage: 'View in Discover',
}),
onClick: viewInStreamLinkProps.onClick!,
href: viewInStreamLinkProps.href,

Some files were not shown because too many files have changed in this diff Show more