From e8a22c053dc96fe8f5776bdfe8b8d1db0e2b5470 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Thu, 24 Oct 2024 15:49:27 +0100 Subject: [PATCH] [Logs Overview] Add a flyout to show category document examples (#194867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements https://github.com/elastic/kibana/issues/193450. ## Discover changes ⚠️ As part of this we need to render a basic table with the log level and summary columns, which is technically context aware but only in the sense we know we want it to be a logs context up front. The "correct" solution here (or at least from recent conversations) is to use the saved search embeddable. There is upcoming work planned to move log stream component usages over to the saved search embeddable. However, currently this isn't in a place to just be dropped in without some pretty extensive work. I didn't feel comfortable doing a big push on that work as a side effort to this work, especially with a loose (if possible) 8.16 aim for this. What I've done (and which isn't ideal I appreciate) is used the start contract of the Discover plugin to export the columns / cells pre-wrapped with the Discover services. It's not ideal in the sense of dependencies, but technically Discover doesn't use logs shared. I considered Discover shared but that's for registering functionality for Discover, rather than the other way around. Eventually we'll be able to remove this and convert over to the new solution. I'm all ears to a better solution, but there's a big mismatch between the needs here and dropping in something that exists currently. Thankfully the changeset for Discover is small if we're happy to keep this temporarily. Edit: I've made some notes here: https://github.com/elastic/logs-dev/issues/111#issuecomment-2411096251 Edit: New package added here: https://github.com/elastic/kibana/commit/c290819c1c1e1cb5a67d437cca7783c0e2302c8f ## Overview From a high level: - Adds a new state machine for handling "details" to show in the flyout (document examples now, plus details and a timeline later). - Hooks this up to a flyout expanded from the categories table. - Provides linking to Discover to view documents from the category in the flyout. I've also left some comments inline. ## UI / UX ![Screenshot 2024-10-10 at 15 05 21](https://github.com/user-attachments/assets/49b525b1-f730-4e90-9a84-05175edb8c40) ![flyout_open](https://github.com/user-attachments/assets/0995b952-566b-4e09-80cf-20ad94343980) ![discover_link](https://github.com/user-attachments/assets/249ef269-0105-48af-9c81-ebae1cfb1680) --------- Co-authored-by: Felix Stürmer Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine Co-authored-by: Felix Stürmer Co-authored-by: Julia Rechkunova (cherry picked from commit 6b63f7f6314e9c05525df32629be7ba769c6ab4c) --- .github/CODEOWNERS | 1 + .i18nrc.json | 2 +- package.json | 1 + .../README.md | 3 + .../index.ts | 7 +- .../jest.config.js | 14 + .../kibana.jsonc | 5 + .../package.json | 7 + .../logs/components}/cell_actions_popover.tsx | 64 ++-- .../src/data_types/logs/components/index.ts | 12 + .../log_level_badge_cell.test.tsx | 4 +- .../log_level_badge_cell.tsx | 4 +- .../service_name_badge_with_actions.tsx | 17 +- .../components}/summary_column/content.tsx | 4 +- .../logs/components/summary_column/index.ts | 13 + .../components}/summary_column/resource.tsx | 5 +- .../summary_column/summary_column.test.tsx | 50 +-- .../summary_column/summary_column.tsx | 171 ++++++++++ .../logs/components/summary_column/utils.tsx | 147 +++++++++ .../logs/components/translations.tsx | 72 +++++ .../src/index.ts | 16 + .../tsconfig.json | 37 +++ packages/kbn-discover-utils/index.ts | 4 +- .../logs/components/{index.ts => index.tsx} | 0 .../src/data_types/logs/constants.ts | 70 ++++ .../src/data_types/logs/index.ts | 2 +- .../src/data_types/logs/types.ts | 7 + .../utils/get_available_resource_fields.ts | 4 +- .../src/data_types/logs/utils/index.ts | 1 + packages/kbn-discover-utils/tsconfig.json | 2 +- .../common/data_types/logs/constants.ts | 62 +--- .../data_types/logs/service_name_cell.tsx | 6 +- .../data_types/logs/summary_column/index.tsx | 8 +- .../logs/summary_column/summary_column.tsx | 172 +--------- .../data_types/logs/summary_column/utils.tsx | 126 -------- .../data_types/logs/translations.tsx | 305 ------------------ .../accessors/get_cell_renderers.tsx | 2 +- src/plugins/discover/tsconfig.json | 4 +- src/plugins/unified_doc_viewer/kibana.jsonc | 1 + tsconfig.base.json | 2 + .../discover_link/discover_link.tsx | 16 +- .../log_categories/log_categories.tsx | 49 ++- .../log_categories_control_bar.tsx | 13 +- .../log_categories/log_categories_grid.tsx | 12 + .../log_categories_grid_cell.tsx | 2 +- .../log_categories_grid_control_columns.tsx | 45 +++ .../log_categories_grid_expand_button.tsx | 71 ++++ .../log_categories_grid_pattern_cell.tsx | 37 +-- .../log_categories_result_content.tsx | 38 ++- .../log_category_details_error_content.tsx | 41 +++ .../log_category_details_flyout.tsx | 139 ++++++++ .../log_category_details_loading_content.tsx | 19 ++ .../log_category_document_examples_table.tsx | 151 +++++++++ .../logs_overview/logs_overview.tsx | 10 +- .../shared/log_category_pattern.tsx | 50 +++ .../category_details_service.ts | 191 +++++++++++ .../category_documents.ts | 63 ++++ .../category_details_service/index.ts | 8 + .../category_details_service/queries.ts | 58 ++++ .../category_details_service/types.ts | 31 ++ .../logs_overview/src/utils/log_category.ts | 12 + .../logs_overview/src/utils/logs_source.ts | 53 ++- .../observability/logs_overview/tsconfig.json | 8 +- .../logs_shared/kibana.jsonc | 3 +- .../public/{plugin.ts => plugin.tsx} | 4 + .../logs_shared/public/types.ts | 2 + .../logs_shared/tsconfig.json | 1 + .../translations/translations/fr-FR.json | 35 -- .../translations/translations/ja-JP.json | 35 -- .../translations/translations/zh-CN.json | 35 -- yarn.lock | 4 + 71 files changed, 1766 insertions(+), 904 deletions(-) create mode 100644 packages/kbn-discover-contextual-components/README.md rename src/plugins/discover/common/data_types/logs/display_options.ts => packages/kbn-discover-contextual-components/index.ts (75%) create mode 100644 packages/kbn-discover-contextual-components/jest.config.js create mode 100644 packages/kbn-discover-contextual-components/kibana.jsonc create mode 100644 packages/kbn-discover-contextual-components/package.json rename {src/plugins/discover/public/components/data_types/logs => packages/kbn-discover-contextual-components/src/data_types/logs/components}/cell_actions_popover.tsx (75%) create mode 100644 packages/kbn-discover-contextual-components/src/data_types/logs/components/index.ts rename {src/plugins/discover/public/components/data_types/logs => packages/kbn-discover-contextual-components/src/data_types/logs/components/log_level_badge_cell}/log_level_badge_cell.test.tsx (93%) rename {src/plugins/discover/public/components/data_types/logs => packages/kbn-discover-contextual-components/src/data_types/logs/components/log_level_badge_cell}/log_level_badge_cell.tsx (92%) rename {src/plugins/discover/public/components/data_types/logs => packages/kbn-discover-contextual-components/src/data_types/logs/components}/service_name_badge_with_actions.tsx (80%) rename {src/plugins/discover/public/components/data_types/logs => packages/kbn-discover-contextual-components/src/data_types/logs/components}/summary_column/content.tsx (95%) create mode 100644 packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/index.ts rename {src/plugins/discover/public/components/data_types/logs => packages/kbn-discover-contextual-components/src/data_types/logs/components}/summary_column/resource.tsx (89%) rename {src/plugins/discover/public/components/data_types/logs => packages/kbn-discover-contextual-components/src/data_types/logs/components}/summary_column/summary_column.test.tsx (86%) create mode 100644 packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/summary_column.tsx create mode 100644 packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/utils.tsx create mode 100644 packages/kbn-discover-contextual-components/src/data_types/logs/components/translations.tsx create mode 100644 packages/kbn-discover-contextual-components/src/index.ts create mode 100644 packages/kbn-discover-contextual-components/tsconfig.json rename packages/kbn-discover-utils/src/data_types/logs/components/{index.ts => index.tsx} (100%) create mode 100644 packages/kbn-discover-utils/src/data_types/logs/constants.ts rename {src/plugins/discover/public => packages/kbn-discover-utils/src/data_types/logs}/utils/get_available_resource_fields.ts (87%) delete mode 100644 src/plugins/discover/public/components/data_types/logs/summary_column/utils.tsx delete mode 100644 src/plugins/discover/public/components/data_types/logs/translations.tsx create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_control_columns.tsx create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_expand_button.tsx create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_error_content.tsx create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_flyout.tsx create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_loading_content.tsx create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_document_examples_table.tsx create mode 100644 x-pack/packages/observability/logs_overview/src/components/shared/log_category_pattern.tsx create mode 100644 x-pack/packages/observability/logs_overview/src/services/category_details_service/category_details_service.ts create mode 100644 x-pack/packages/observability/logs_overview/src/services/category_details_service/category_documents.ts create mode 100644 x-pack/packages/observability/logs_overview/src/services/category_details_service/index.ts create mode 100644 x-pack/packages/observability/logs_overview/src/services/category_details_service/queries.ts create mode 100644 x-pack/packages/observability/logs_overview/src/services/category_details_service/types.ts create mode 100644 x-pack/packages/observability/logs_overview/src/utils/log_category.ts rename x-pack/plugins/observability_solution/logs_shared/public/{plugin.ts => plugin.tsx} (97%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1ea2730e6c22d..ddc5535d29008 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -385,6 +385,7 @@ packages/kbn-dev-proc-runner @elastic/kibana-operations src/plugins/dev_tools @elastic/kibana-management packages/kbn-dev-utils @elastic/kibana-operations examples/developer_examples @elastic/appex-sharedux +packages/kbn-discover-contextual-components @elastic/obs-ux-logs-team @elastic/kibana-data-discovery examples/discover_customization_examples @elastic/kibana-data-discovery x-pack/plugins/discover_enhanced @elastic/kibana-data-discovery src/plugins/discover @elastic/kibana-data-discovery diff --git a/.i18nrc.json b/.i18nrc.json index 036be597ac969..5c7642e6283eb 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -27,7 +27,7 @@ "dataViews": "src/plugins/data_views", "defaultNavigation": "packages/default-nav", "devTools": "src/plugins/dev_tools", - "discover": ["src/plugins/discover", "packages/kbn-discover-utils"], + "discover": ["src/plugins/discover", "packages/kbn-discover-utils", "packages/kbn-discover-contextual-components"], "savedSearch": "src/plugins/saved_search", "embeddableApi": "src/plugins/embeddable", "presentationPanel": "src/plugins/presentation_panel", diff --git a/package.json b/package.json index a5ac1fe7f6d64..d3938c64969fb 100644 --- a/package.json +++ b/package.json @@ -451,6 +451,7 @@ "@kbn/default-nav-ml": "link:packages/default-nav/ml", "@kbn/dev-tools-plugin": "link:src/plugins/dev_tools", "@kbn/developer-examples-plugin": "link:examples/developer_examples", + "@kbn/discover-contextual-components": "link:packages/kbn-discover-contextual-components", "@kbn/discover-customization-examples-plugin": "link:examples/discover_customization_examples", "@kbn/discover-enhanced-plugin": "link:x-pack/plugins/discover_enhanced", "@kbn/discover-plugin": "link:src/plugins/discover", diff --git a/packages/kbn-discover-contextual-components/README.md b/packages/kbn-discover-contextual-components/README.md new file mode 100644 index 0000000000000..ae9e2402c2a69 --- /dev/null +++ b/packages/kbn-discover-contextual-components/README.md @@ -0,0 +1,3 @@ +# @kbn/discover-contextual-components + +Houses contextual (e.g. logs) components that are used by Discover. diff --git a/src/plugins/discover/common/data_types/logs/display_options.ts b/packages/kbn-discover-contextual-components/index.ts similarity index 75% rename from src/plugins/discover/common/data_types/logs/display_options.ts rename to packages/kbn-discover-contextual-components/index.ts index 05803ba0bde7f..55b900ad5137a 100644 --- a/src/plugins/discover/common/data_types/logs/display_options.ts +++ b/packages/kbn-discover-contextual-components/index.ts @@ -7,9 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export interface SmartFieldGridColumnOptions { - type: 'smart-field'; - smartField: 'content' | 'resource'; - fallbackFields: string[]; - width?: number; -} +export * from './src'; diff --git a/packages/kbn-discover-contextual-components/jest.config.js b/packages/kbn-discover-contextual-components/jest.config.js new file mode 100644 index 0000000000000..bacfd33649ce4 --- /dev/null +++ b/packages/kbn-discover-contextual-components/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-discover-contextual-components'], +}; diff --git a/packages/kbn-discover-contextual-components/kibana.jsonc b/packages/kbn-discover-contextual-components/kibana.jsonc new file mode 100644 index 0000000000000..cfb9b1d5431ef --- /dev/null +++ b/packages/kbn-discover-contextual-components/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-browser", + "id": "@kbn/discover-contextual-components", + "owner": ["@elastic/obs-ux-logs-team", "@elastic/kibana-data-discovery"] +} diff --git a/packages/kbn-discover-contextual-components/package.json b/packages/kbn-discover-contextual-components/package.json new file mode 100644 index 0000000000000..4a63d975cda42 --- /dev/null +++ b/packages/kbn-discover-contextual-components/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/discover-contextual-components", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0", + "sideEffects": false +} \ No newline at end of file diff --git a/src/plugins/discover/public/components/data_types/logs/cell_actions_popover.tsx b/packages/kbn-discover-contextual-components/src/data_types/logs/components/cell_actions_popover.tsx similarity index 75% rename from src/plugins/discover/public/components/data_types/logs/cell_actions_popover.tsx rename to packages/kbn-discover-contextual-components/src/data_types/logs/components/cell_actions_popover.tsx index 7b9d68e8f3dd7..96651cf26189b 100644 --- a/src/plugins/discover/public/components/data_types/logs/cell_actions_popover.tsx +++ b/packages/kbn-discover-contextual-components/src/data_types/logs/components/cell_actions_popover.tsx @@ -24,7 +24,9 @@ import { import { css } from '@emotion/react'; import { useBoolean } from '@kbn/react-hooks'; import { euiThemeVars } from '@kbn/ui-theme'; -import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { actionFilterForText, actionFilterOutText, @@ -109,30 +111,32 @@ export function CellActionsPopover({ /> - - - - {filterForText} - - - {filterOutText} - - - + {onFilter ? ( + + + + {filterForText} + + + {filterOutText} + + + + ) : null} {(copy) => ( @@ -158,13 +162,21 @@ export interface FieldBadgeWithActionsProps icon?: EuiBadgeProps['iconType']; } +interface FieldBadgeWithActionsDependencies { + core?: CoreStart; + share?: SharePluginStart; +} + +export type FieldBadgeWithActionsPropsAndDependencies = FieldBadgeWithActionsProps & + FieldBadgeWithActionsDependencies; + export function FieldBadgeWithActions({ icon, onFilter, property, renderValue, value, -}: FieldBadgeWithActionsProps) { +}: FieldBadgeWithActionsPropsAndDependencies) { return ( { const LogLevelBadgeCell = getLogLevelBadgeCell(logLevelField); diff --git a/src/plugins/discover/public/components/data_types/logs/log_level_badge_cell.tsx b/packages/kbn-discover-contextual-components/src/data_types/logs/components/log_level_badge_cell/log_level_badge_cell.tsx similarity index 92% rename from src/plugins/discover/public/components/data_types/logs/log_level_badge_cell.tsx rename to packages/kbn-discover-contextual-components/src/data_types/logs/components/log_level_badge_cell/log_level_badge_cell.tsx index bff3bdddee026..4223f1e0de5c1 100644 --- a/src/plugins/discover/public/components/data_types/logs/log_level_badge_cell.tsx +++ b/packages/kbn-discover-contextual-components/src/data_types/logs/components/log_level_badge_cell/log_level_badge_cell.tsx @@ -9,8 +9,8 @@ import type { CSSObject } from '@emotion/react'; import React from 'react'; +import type { DataGridCellValueElementProps } from '@kbn/unified-data-table/src/types'; import { LogLevelBadge } from '@kbn/discover-utils'; -import type { DataGridCellValueElementProps } from '@kbn/unified-data-table'; const dataTestSubj = 'logLevelBadgeCell'; const badgeCss: CSSObject = { marginTop: '-4px' }; @@ -32,3 +32,5 @@ export const getLogLevelBadgeCell = /> ); }; + +export type LogLevelBadgeCell = ReturnType; diff --git a/src/plugins/discover/public/components/data_types/logs/service_name_badge_with_actions.tsx b/packages/kbn-discover-contextual-components/src/data_types/logs/components/service_name_badge_with_actions.tsx similarity index 80% rename from src/plugins/discover/public/components/data_types/logs/service_name_badge_with_actions.tsx rename to packages/kbn-discover-contextual-components/src/data_types/logs/components/service_name_badge_with_actions.tsx index 581c889b8e98e..7916b1144d851 100644 --- a/src/plugins/discover/public/components/data_types/logs/service_name_badge_with_actions.tsx +++ b/packages/kbn-discover-contextual-components/src/data_types/logs/components/service_name_badge_with_actions.tsx @@ -11,17 +11,20 @@ import React from 'react'; import { getRouterLinkProps } from '@kbn/router-utils'; import { EuiLink } from '@elastic/eui'; import { OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE } from '@kbn/management-settings-ids'; -import { SharePublicStart } from '@kbn/share-plugin/public/plugin'; -import { useDiscoverServices } from '../../../hooks/use_discover_services'; -import { FieldBadgeWithActions, FieldBadgeWithActionsProps } from './cell_actions_popover'; +import type { SharePublicStart } from '@kbn/share-plugin/public/plugin'; +import { + FieldBadgeWithActions, + FieldBadgeWithActionsProps, + FieldBadgeWithActionsPropsAndDependencies, +} from './cell_actions_popover'; const SERVICE_ENTITY_LOCATOR = 'SERVICE_ENTITY_LOCATOR'; -export function ServiceNameBadgeWithActions(props: FieldBadgeWithActionsProps) { - const { share, core } = useDiscoverServices(); - const canViewApm = core.application.capabilities.apm?.show || false; +export function ServiceNameBadgeWithActions(props: FieldBadgeWithActionsPropsAndDependencies) { + const { share, core } = props; + const canViewApm = core?.application.capabilities.apm?.show || false; const isEntityCentricExperienceSettingEnabled = canViewApm - ? core.uiSettings.get(OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE) + ? core?.uiSettings.get(OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE) : false; const derivedPropsForEntityExperience = isEntityCentricExperienceSettingEnabled diff --git a/src/plugins/discover/public/components/data_types/logs/summary_column/content.tsx b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/content.tsx similarity index 95% rename from src/plugins/discover/public/components/data_types/logs/summary_column/content.tsx rename to packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/content.tsx index 0da98cbf7145e..cc576efff17db 100644 --- a/src/plugins/discover/public/components/data_types/logs/summary_column/content.tsx +++ b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/content.tsx @@ -14,7 +14,7 @@ import { getLogDocumentOverview, getMessageFieldWithFallbacks, } from '@kbn/discover-utils'; -import * as constants from '../../../../../common/data_types/logs/constants'; +import { MESSAGE_FIELD } from '@kbn/discover-utils'; import { formatJsonDocumentForContent } from './utils'; interface ContentProps extends DataGridCellValueElementProps { @@ -32,7 +32,7 @@ const LogMessage = ({ value: string; className: string; }) => { - const shouldRenderFieldName = field !== constants.MESSAGE_FIELD; + const shouldRenderFieldName = field !== MESSAGE_FIELD; if (shouldRenderFieldName) { return ( diff --git a/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/index.ts b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/index.ts new file mode 100644 index 0000000000000..006ec34d0a475 --- /dev/null +++ b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './content'; +export * from './resource'; +export * from './summary_column'; +export * from './utils'; diff --git a/src/plugins/discover/public/components/data_types/logs/summary_column/resource.tsx b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/resource.tsx similarity index 89% rename from src/plugins/discover/public/components/data_types/logs/summary_column/resource.tsx rename to packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/resource.tsx index a7955fadde622..5ea7ddda7a6b7 100644 --- a/src/plugins/discover/public/components/data_types/logs/summary_column/resource.tsx +++ b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/resource.tsx @@ -8,8 +8,8 @@ */ import React from 'react'; -import { EuiBadge, EuiFlexGroup } from '@elastic/eui'; -import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { CommonProps, EuiBadge, EuiFlexGroup } from '@elastic/eui'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import { ResourceFieldDescriptor } from './utils'; const MAX_LIMITED_FIELDS_VISIBLE = 3; @@ -19,6 +19,7 @@ interface ResourceProps { /* When true, the column will render a predefined number of resources and indicates with a badge how many more we have */ limited?: boolean; onFilter?: DocViewFilterFn; + css?: CommonProps['css']; } export const Resource = ({ fields, limited = false, onFilter, ...props }: ResourceProps) => { diff --git a/src/plugins/discover/public/components/data_types/logs/summary_column/summary_column.test.tsx b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/summary_column.test.tsx similarity index 86% rename from src/plugins/discover/public/components/data_types/logs/summary_column/summary_column.test.tsx rename to packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/summary_column.test.tsx index b8eeea613c9c6..6b337167279e3 100644 --- a/src/plugins/discover/public/components/data_types/logs/summary_column/summary_column.test.tsx +++ b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/summary_column.test.tsx @@ -8,41 +8,41 @@ */ import React from 'react'; -import { buildDataTableRecord, DataTableRecord } from '@kbn/discover-utils'; -import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; import { render, screen } from '@testing-library/react'; import SummaryColumn, { SummaryColumnFactoryDeps, SummaryColumnProps } from './summary_column'; import { DataGridDensity, ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table'; -import * as constants from '../../../../../common/data_types/logs/constants'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { discoverServiceMock } from '../../../../__mocks__/services'; +import * as constants from '@kbn/discover-utils/src/data_types/logs/constants'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; +import { coreMock as corePluginMock } from '@kbn/core/public/mocks'; +import { DataTableRecord, buildDataTableRecord } from '@kbn/discover-utils'; +import { dataViewMock } from '@kbn/discover-utils/src/__mocks__/data_view'; const renderSummary = ( record: DataTableRecord, opts: Partial = {} ) => { render( - - {}} - closePopover={() => {}} - density={DataGridDensity.COMPACT} - rowHeight={ROWS_HEIGHT_OPTIONS.single} - onFilter={jest.fn()} - shouldShowFieldHandler={() => true} - {...opts} - /> - + {}} + closePopover={() => {}} + density={DataGridDensity.COMPACT} + rowHeight={ROWS_HEIGHT_OPTIONS.single} + onFilter={jest.fn()} + shouldShowFieldHandler={() => true} + core={corePluginMock.createStart()} + share={sharePluginMock.createStartContract()} + {...opts} + /> ); }; diff --git a/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/summary_column.tsx b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/summary_column.tsx new file mode 100644 index 0000000000000..98f772fcf41d1 --- /dev/null +++ b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/summary_column.tsx @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { DataGridDensity, type DataGridCellValueElementProps } from '@kbn/unified-data-table'; +import React from 'react'; +import { EuiButtonIcon, EuiCodeBlock, EuiFlexGroup, EuiText, EuiTitle } from '@elastic/eui'; +import { JsonCodeEditor } from '@kbn/unified-doc-viewer-plugin/public'; +import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import { + ShouldShowFieldInTableHandler, + getLogDocumentOverview, + getMessageFieldWithFallbacks, +} from '@kbn/discover-utils'; +import { ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table'; +import { Resource } from './resource'; +import { Content } from './content'; +import { createResourceFields, formatJsonDocumentForContent } from './utils'; +import { + closeCellActionPopoverText, + contentLabel, + jsonLabel, + resourceLabel, +} from '../translations'; + +export interface SummaryColumnFactoryDeps { + density: DataGridDensity | undefined; + rowHeight: number | undefined; + shouldShowFieldHandler: ShouldShowFieldInTableHandler; + onFilter?: DocViewFilterFn; + core: CoreStart; + share?: SharePluginStart; +} + +export type SummaryColumnProps = DataGridCellValueElementProps; +export type AllSummaryColumnProps = SummaryColumnProps & SummaryColumnFactoryDeps; + +export const SummaryColumn = (props: AllSummaryColumnProps) => { + const { isDetails } = props; + + if (isDetails) { + return ; + } + + return ; +}; + +// eslint-disable-next-line import/no-default-export +export default SummaryColumn; + +const SummaryCell = ({ + density: maybeNullishDensity, + rowHeight: maybeNullishRowHeight, + ...props +}: AllSummaryColumnProps) => { + const { onFilter, row, share, core } = props; + + const density = maybeNullishDensity ?? DataGridDensity.COMPACT; + const isCompressed = density === DataGridDensity.COMPACT; + + const rowHeight = maybeNullishRowHeight ?? ROWS_HEIGHT_OPTIONS.single; + const isSingleLine = rowHeight === ROWS_HEIGHT_OPTIONS.single || rowHeight === 1; + + const resourceFields = createResourceFields(row, core, share); + const shouldRenderResource = resourceFields.length > 0; + + return isSingleLine ? ( + + {shouldRenderResource && ( + + )} + + + ) : ( + <> + {shouldRenderResource && ( + + )} + + + ); +}; + +const SummaryCellPopover = (props: AllSummaryColumnProps) => { + const { row, dataView, fieldFormats, onFilter, closePopover, share, core } = props; + + const resourceFields = createResourceFields(row, core, share); + const shouldRenderResource = resourceFields.length > 0; + + const documentOverview = getLogDocumentOverview(row, { dataView, fieldFormats }); + const { field, value } = getMessageFieldWithFallbacks(documentOverview); + const shouldRenderContent = Boolean(field && value); + + const shouldRenderSource = !shouldRenderContent; + + return ( + + + {shouldRenderResource && ( + + + {resourceLabel} + + + + )} + + + {contentLabel} + + {shouldRenderContent && ( + + + {field} + + + {value} + + + )} + {shouldRenderSource && ( + + + {jsonLabel} + + + + )} + + + ); +}; + +const singleLineResourceCss = { + flexGrow: 0, + lineHeight: 'normal', + marginTop: -1, +}; + +const multiLineResourceCss = { display: 'inline-flex' }; diff --git a/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/utils.tsx b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/utils.tsx new file mode 100644 index 0000000000000..7dacc3393763e --- /dev/null +++ b/packages/kbn-discover-contextual-components/src/data_types/logs/components/summary_column/utils.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { dynamic } from '@kbn/shared-ux-utility'; +import React from 'react'; +import { css } from '@emotion/react'; +import { AgentName } from '@kbn/elastic-agent-utils'; +import { euiThemeVars } from '@kbn/ui-theme'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import { + AGENT_NAME_FIELD, + CLOUD_INSTANCE_ID_FIELD, + CONTAINER_ID_FIELD, + CONTAINER_NAME_FIELD, + FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT, + HOST_NAME_FIELD, + ORCHESTRATOR_CLUSTER_NAME_FIELD, + ORCHESTRATOR_NAMESPACE_FIELD, + ORCHESTRATOR_RESOURCE_ID_FIELD, + SERVICE_NAME_FIELD, +} from '@kbn/discover-utils'; +import { DataTableRecord, getFieldValue } from '@kbn/discover-utils'; +import { LogDocument, ResourceFields, getAvailableResourceFields } from '@kbn/discover-utils/src'; +import { FieldBadgeWithActions, FieldBadgeWithActionsProps } from '../cell_actions_popover'; +import { ServiceNameBadgeWithActions } from '../service_name_badge_with_actions'; +/** + * getUnformattedResourceFields definitions + */ +export const getUnformattedResourceFields = (doc: LogDocument): ResourceFields => { + const serviceName = getFieldValue(doc, SERVICE_NAME_FIELD); + const hostName = getFieldValue(doc, HOST_NAME_FIELD); + const agentName = getFieldValue(doc, AGENT_NAME_FIELD); + const orchestratorClusterName = getFieldValue(doc, ORCHESTRATOR_CLUSTER_NAME_FIELD); + const orchestratorResourceId = getFieldValue(doc, ORCHESTRATOR_RESOURCE_ID_FIELD); + const orchestratorNamespace = getFieldValue(doc, ORCHESTRATOR_NAMESPACE_FIELD); + const containerName = getFieldValue(doc, CONTAINER_NAME_FIELD); + const containerId = getFieldValue(doc, CONTAINER_ID_FIELD); + const cloudInstanceId = getFieldValue(doc, CLOUD_INSTANCE_ID_FIELD); + + return { + [SERVICE_NAME_FIELD]: serviceName, + [HOST_NAME_FIELD]: hostName, + [AGENT_NAME_FIELD]: agentName, + [ORCHESTRATOR_CLUSTER_NAME_FIELD]: orchestratorClusterName, + [ORCHESTRATOR_RESOURCE_ID_FIELD]: orchestratorResourceId, + [ORCHESTRATOR_NAMESPACE_FIELD]: orchestratorNamespace, + [CONTAINER_NAME_FIELD]: containerName, + [CONTAINER_ID_FIELD]: containerId, + [CLOUD_INSTANCE_ID_FIELD]: cloudInstanceId, + }; +}; + +/** + * createResourceFields definitions + */ +const AgentIcon = dynamic(() => import('@kbn/custom-icons/src/components/agent_icon')); + +const resourceCustomComponentsMap: Partial< + Record> +> = { + [SERVICE_NAME_FIELD]: ServiceNameBadgeWithActions, +}; + +export interface ResourceFieldDescriptor { + ResourceBadge: React.ComponentType; + Icon?: () => JSX.Element; + name: keyof ResourceFields; + value: string; +} + +export const createResourceFields = ( + row: DataTableRecord, + core: CoreStart, + share?: SharePluginStart +): ResourceFieldDescriptor[] => { + const resourceDoc = getUnformattedResourceFields(row as LogDocument); + + const availableResourceFields = getAvailableResourceFields(resourceDoc); + + const resourceFields = availableResourceFields.map((name) => { + const ResourceBadgeComponent = resourceCustomComponentsMap[name] ?? FieldBadgeWithActions; + const resourceBadgeComponentWithDependencies = (props: FieldBadgeWithActionsProps) => ( + + ); + return { + name, + value: resourceDoc[name] as string, + ResourceBadge: resourceBadgeComponentWithDependencies, + ...(name === SERVICE_NAME_FIELD && { + Icon: () => ( + + ), + }), + }; + }); + + return resourceFields; +}; + +/** + * formatJsonDocumentForContent definitions + */ +export const formatJsonDocumentForContent = (row: DataTableRecord) => { + const flattenedResult: DataTableRecord['flattened'] = {}; + const rawFieldResult: DataTableRecord['raw']['fields'] = {}; + const { raw, flattened } = row; + const { fields } = raw; + + // We need 2 loops here for flattened and raw.fields. Flattened contains all fields, + // whereas raw.fields only contains certain fields excluding _ignored + for (const fieldName in flattened) { + if (isFieldAllowed(fieldName) && flattened[fieldName]) { + flattenedResult[fieldName] = flattened[fieldName]; + } + } + + for (const fieldName in fields) { + if (isFieldAllowed(fieldName) && fields[fieldName]) { + rawFieldResult[fieldName] = fields[fieldName]; + } + } + + return { + ...row, + flattened: flattenedResult, + raw: { + ...raw, + fields: rawFieldResult, + }, + }; +}; + +const isFieldAllowed = (field: string) => + !FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT.some((prefix) => field.startsWith(prefix)); diff --git a/packages/kbn-discover-contextual-components/src/data_types/logs/components/translations.tsx b/packages/kbn-discover-contextual-components/src/data_types/logs/components/translations.tsx new file mode 100644 index 0000000000000..52e083f8b86b8 --- /dev/null +++ b/packages/kbn-discover-contextual-components/src/data_types/logs/components/translations.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; + +export const jsonLabel = i18n.translate('discover.logs.dataTable.header.popover.json', { + defaultMessage: 'JSON', +}); + +export const contentLabel = i18n.translate('discover.logs.dataTable.header.popover.content', { + defaultMessage: 'Content', +}); + +export const resourceLabel = i18n.translate('discover.logs.dataTable.header.popover.resource', { + defaultMessage: 'Resource', +}); + +export const actionFilterForText = (text: string) => + i18n.translate('discover.logs.flyoutDetail.value.hover.filterFor', { + defaultMessage: 'Filter for this {value}', + values: { + value: text, + }, + }); + +export const actionFilterOutText = (text: string) => + i18n.translate('discover.logs.flyoutDetail.value.hover.filterOut', { + defaultMessage: 'Filter out this {value}', + values: { + value: text, + }, + }); + +export const filterOutText = i18n.translate('discover.logs.popoverAction.filterOut', { + defaultMessage: 'Filter out', +}); + +export const filterForText = i18n.translate('discover.logs.popoverAction.filterFor', { + defaultMessage: 'Filter for', +}); + +export const copyValueText = i18n.translate('discover.logs.popoverAction.copyValue', { + defaultMessage: 'Copy value', +}); + +export const copyValueAriaText = (fieldName: string) => + i18n.translate('discover.logs.popoverAction.copyValueAriaText', { + defaultMessage: 'Copy value of {fieldName}', + values: { + fieldName, + }, + }); + +export const openCellActionPopoverAriaText = i18n.translate( + 'discover.logs.popoverAction.openPopover', + { + defaultMessage: 'Open popover', + } +); + +export const closeCellActionPopoverText = i18n.translate( + 'discover.logs.popoverAction.closePopover', + { + defaultMessage: 'Close popover', + } +); diff --git a/packages/kbn-discover-contextual-components/src/index.ts b/packages/kbn-discover-contextual-components/src/index.ts new file mode 100644 index 0000000000000..52ee5931aa4fc --- /dev/null +++ b/packages/kbn-discover-contextual-components/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { dynamic } from '@kbn/shared-ux-utility'; + +export * from './data_types/logs/components'; + +export const LazySummaryColumn = dynamic( + () => import('./data_types/logs/components/summary_column/summary_column') +); diff --git a/packages/kbn-discover-contextual-components/tsconfig.json b/packages/kbn-discover-contextual-components/tsconfig.json new file mode 100644 index 0000000000000..21d65228b9597 --- /dev/null +++ b/packages/kbn-discover-contextual-components/tsconfig.json @@ -0,0 +1,37 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "@testing-library/jest-dom", + "@testing-library/react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/field-formats-plugin", + "@kbn/discover-utils", + "@kbn/router-utils", + "@kbn/management-settings-ids", + "@kbn/share-plugin", + "@kbn/ui-theme", + "@kbn/unified-data-table", + "@kbn/unified-doc-viewer", + "@kbn/react-hooks", + "@kbn/core-lifecycle-browser", + "@kbn/i18n", + "@kbn/unified-doc-viewer-plugin", + "@kbn/core", + "@kbn/shared-ux-utility", + "@kbn/elastic-agent-utils", + "@kbn/custom-icons", + ] +} diff --git a/packages/kbn-discover-utils/index.ts b/packages/kbn-discover-utils/index.ts index ed6d58ca3da8d..7234944783037 100644 --- a/packages/kbn-discover-utils/index.ts +++ b/packages/kbn-discover-utils/index.ts @@ -52,15 +52,17 @@ export { getLogLevelCoalescedValue, getLogLevelCoalescedValueLabel, LogLevelCoalescedValue, - LogLevelBadge, getFieldValue, getVisibleColumns, canPrependTimeFieldColumn, DiscoverFlyouts, dismissAllFlyoutsExceptFor, dismissFlyouts, + LogLevelBadge, } from './src'; export type { LogsContextService } from './src'; export * from './src/types'; + +export * from './src/data_types/logs/constants'; diff --git a/packages/kbn-discover-utils/src/data_types/logs/components/index.ts b/packages/kbn-discover-utils/src/data_types/logs/components/index.tsx similarity index 100% rename from packages/kbn-discover-utils/src/data_types/logs/components/index.ts rename to packages/kbn-discover-utils/src/data_types/logs/components/index.tsx diff --git a/packages/kbn-discover-utils/src/data_types/logs/constants.ts b/packages/kbn-discover-utils/src/data_types/logs/constants.ts new file mode 100644 index 0000000000000..82edebaff0e81 --- /dev/null +++ b/packages/kbn-discover-utils/src/data_types/logs/constants.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { fieldConstants } from '../..'; +import { SmartFieldGridColumnOptions } from './types'; + +export * from '../../field_constants'; + +export const LOGS_EXPLORER_PROFILE_ID = 'logs-explorer'; + +// Virtual column fields +export const CONTENT_FIELD = 'content'; +export const RESOURCE_FIELD = 'resource'; + +// Sizing +export const DATA_GRID_COLUMN_WIDTH_SMALL = 240; +export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320; +export const ACTIONS_COLUMN_WIDTH = 80; + +export const RESOURCE_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = { + type: 'smart-field', + smartField: RESOURCE_FIELD, + fallbackFields: [fieldConstants.HOST_NAME_FIELD, fieldConstants.SERVICE_NAME_FIELD], + width: DATA_GRID_COLUMN_WIDTH_MEDIUM, +}; + +export const CONTENT_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = { + type: 'smart-field', + smartField: CONTENT_FIELD, + fallbackFields: [fieldConstants.MESSAGE_FIELD], +}; + +export const SMART_FALLBACK_FIELDS = { + [CONTENT_FIELD]: CONTENT_FIELD_CONFIGURATION, + [RESOURCE_FIELD]: RESOURCE_FIELD_CONFIGURATION, +}; + +// UI preferences +export const DEFAULT_COLUMNS = [RESOURCE_FIELD_CONFIGURATION, CONTENT_FIELD_CONFIGURATION]; +export const DEFAULT_ROWS_PER_PAGE = 100; + +// List of prefixes which needs to be filtered out for Display in Content Column +export const FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT = [ + '_', // Filter fields like '_id', '_score' + '@timestamp', + 'agent.', + 'elastic_agent.', + 'data_stream.', + 'ecs.', + 'host.', + 'container.', + 'cloud.', + 'kubernetes.', + 'orchestrator.', + 'log.', + 'service.', +]; + +export const DEFAULT_ALLOWED_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat']; +export const DEFAULT_ALLOWED_LOGS_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat']; + +export const LOG_LEVEL_FIELDS = ['log.level', 'log_level']; +export const SERVICE_NAME_FIELDS = ['service.name', 'service_name']; +export const AGENT_NAME_FIELD = 'agent.name'; diff --git a/packages/kbn-discover-utils/src/data_types/logs/index.ts b/packages/kbn-discover-utils/src/data_types/logs/index.ts index 7ec996ee31010..30b023b6328bb 100644 --- a/packages/kbn-discover-utils/src/data_types/logs/index.ts +++ b/packages/kbn-discover-utils/src/data_types/logs/index.ts @@ -8,7 +8,7 @@ */ export * from './types'; -export * from './components'; export * from './utils'; export * from './logs_context_service'; +export * from './components'; diff --git a/packages/kbn-discover-utils/src/data_types/logs/types.ts b/packages/kbn-discover-utils/src/data_types/logs/types.ts index 843205d6e8b1e..123ad6c631026 100644 --- a/packages/kbn-discover-utils/src/data_types/logs/types.ts +++ b/packages/kbn-discover-utils/src/data_types/logs/types.ts @@ -86,3 +86,10 @@ export interface StackTraceFields { 'error.exception.stacktrace'?: string; 'error.log.stacktrace'?: string; } + +export interface SmartFieldGridColumnOptions { + type: 'smart-field'; + smartField: 'content' | 'resource'; + fallbackFields: string[]; + width?: number; +} diff --git a/src/plugins/discover/public/utils/get_available_resource_fields.ts b/packages/kbn-discover-utils/src/data_types/logs/utils/get_available_resource_fields.ts similarity index 87% rename from src/plugins/discover/public/utils/get_available_resource_fields.ts rename to packages/kbn-discover-utils/src/data_types/logs/utils/get_available_resource_fields.ts index 588194d2a13ca..e59b7a99c9163 100644 --- a/src/plugins/discover/public/utils/get_available_resource_fields.ts +++ b/packages/kbn-discover-utils/src/data_types/logs/utils/get_available_resource_fields.ts @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ResourceFields } from '@kbn/discover-utils/src'; -import * as constants from '../../common/data_types/logs/constants'; +import { ResourceFields } from '../../..'; +import * as constants from '../constants'; export const getAvailableResourceFields = (resourceDoc: ResourceFields) => { const resourceFields: Array = [ diff --git a/packages/kbn-discover-utils/src/data_types/logs/utils/index.ts b/packages/kbn-discover-utils/src/data_types/logs/utils/index.ts index 0b266fa5b4935..365365eb7ac13 100644 --- a/packages/kbn-discover-utils/src/data_types/logs/utils/index.ts +++ b/packages/kbn-discover-utils/src/data_types/logs/utils/index.ts @@ -9,3 +9,4 @@ export * from './get_log_level_color'; export * from './get_log_level_coalesed_value'; +export * from './get_available_resource_fields'; diff --git a/packages/kbn-discover-utils/tsconfig.json b/packages/kbn-discover-utils/tsconfig.json index 90235fada49c5..865603e379eca 100644 --- a/packages/kbn-discover-utils/tsconfig.json +++ b/packages/kbn-discover-utils/tsconfig.json @@ -25,9 +25,9 @@ "@kbn/field-types", "@kbn/i18n", "@kbn/core-ui-settings-browser", - "@kbn/ui-theme", "@kbn/expressions-plugin", "@kbn/logs-data-access-plugin", + "@kbn/ui-theme", "@kbn/i18n-react" ] } diff --git a/src/plugins/discover/common/data_types/logs/constants.ts b/src/plugins/discover/common/data_types/logs/constants.ts index 18259dcc56b28..a9ca3697763f9 100644 --- a/src/plugins/discover/common/data_types/logs/constants.ts +++ b/src/plugins/discover/common/data_types/logs/constants.ts @@ -7,64 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { fieldConstants } from '@kbn/discover-utils'; -import { SmartFieldGridColumnOptions } from './display_options'; - -export * from '@kbn/discover-utils/src/field_constants'; - -export const LOGS_EXPLORER_PROFILE_ID = 'logs-explorer'; - -// Virtual column fields -export const CONTENT_FIELD = 'content'; -export const RESOURCE_FIELD = 'resource'; - -// Sizing -export const DATA_GRID_COLUMN_WIDTH_SMALL = 240; -export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320; -export const ACTIONS_COLUMN_WIDTH = 80; - -export const RESOURCE_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = { - type: 'smart-field', - smartField: RESOURCE_FIELD, - fallbackFields: [fieldConstants.HOST_NAME_FIELD, fieldConstants.SERVICE_NAME_FIELD], - width: DATA_GRID_COLUMN_WIDTH_MEDIUM, -}; - -export const CONTENT_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = { - type: 'smart-field', - smartField: CONTENT_FIELD, - fallbackFields: [fieldConstants.MESSAGE_FIELD], -}; - -export const SMART_FALLBACK_FIELDS = { - [CONTENT_FIELD]: CONTENT_FIELD_CONFIGURATION, - [RESOURCE_FIELD]: RESOURCE_FIELD_CONFIGURATION, -}; - -// UI preferences -export const DEFAULT_COLUMNS = [RESOURCE_FIELD_CONFIGURATION, CONTENT_FIELD_CONFIGURATION]; -export const DEFAULT_ROWS_PER_PAGE = 100; - -// List of prefixes which needs to be filtered out for Display in Content Column -export const FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT = [ - '_', // Filter fields like '_id', '_score' - '@timestamp', - 'agent.', - 'elastic_agent.', - 'data_stream.', - 'ecs.', - 'host.', - 'container.', - 'cloud.', - 'kubernetes.', - 'orchestrator.', - 'log.', - 'service.', -]; - -export const DEFAULT_ALLOWED_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat']; -export const DEFAULT_ALLOWED_LOGS_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat']; - -export const LOG_LEVEL_FIELDS = ['log.level', 'log_level']; -export const SERVICE_NAME_FIELDS = ['service.name', 'service_name']; -export const AGENT_NAME_FIELD = 'agent.name'; +export * from '@kbn/discover-utils/src/data_types/logs/constants'; diff --git a/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx b/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx index cd94cd609dc69..3d543f7f0c954 100644 --- a/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx +++ b/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx @@ -15,9 +15,10 @@ import type { DataGridCellValueElementProps } from '@kbn/unified-data-table'; import { css } from '@emotion/react'; import { getFieldValue } from '@kbn/discover-utils'; import { euiThemeVars } from '@kbn/ui-theme'; +import { ServiceNameBadgeWithActions } from '@kbn/discover-contextual-components'; +import { useDiscoverServices } from '../../../hooks/use_discover_services'; import { CellRenderersExtensionParams } from '../../../context_awareness'; import { AGENT_NAME_FIELD } from '../../../../common/data_types/logs/constants'; -import { ServiceNameBadgeWithActions } from './service_name_badge_with_actions'; const AgentIcon = dynamic(() => import('@kbn/custom-icons/src/components/agent_icon')); const dataTestSubj = 'serviceNameCell'; @@ -28,6 +29,7 @@ const agentIconStyle = css` export const getServiceNameCell = (serviceNameField: string, { actions }: CellRenderersExtensionParams) => (props: DataGridCellValueElementProps) => { + const { core, share } = useDiscoverServices(); const serviceNameValue = getFieldValue(props.row, serviceNameField) as string; const agentName = getFieldValue(props.row, AGENT_NAME_FIELD) as AgentName; @@ -47,6 +49,8 @@ export const getServiceNameCell = icon={getIcon} value={serviceNameValue} property={serviceNameField} + core={core} + share={share} /> ); }; diff --git a/src/plugins/discover/public/components/data_types/logs/summary_column/index.tsx b/src/plugins/discover/public/components/data_types/logs/summary_column/index.tsx index 20fe4380199f3..dbcef4f558b33 100644 --- a/src/plugins/discover/public/components/data_types/logs/summary_column/index.tsx +++ b/src/plugins/discover/public/components/data_types/logs/summary_column/index.tsx @@ -8,13 +8,11 @@ */ import React from 'react'; -import { dynamic } from '@kbn/shared-ux-utility'; import { getShouldShowFieldHandler } from '@kbn/discover-utils'; import { DataView } from '@kbn/data-views-plugin/common'; +import { SummaryColumnProps } from '@kbn/discover-contextual-components'; import { CellRenderersExtensionParams } from '../../../../context_awareness'; -import type { SummaryColumnProps } from './summary_column'; - -const SummaryColumn = dynamic(() => import('./summary_column')); +import { SummaryColumn } from './summary_column'; export type SummaryColumnGetterDeps = CellRenderersExtensionParams; @@ -22,7 +20,7 @@ export const getSummaryColumn = (params: SummaryColumnGetterDeps) => { const { actions, dataView, density, rowHeight } = params; const shouldShowFieldHandler = createGetShouldShowFieldHandler(dataView); - return (props: SummaryColumnProps) => ( + return (props: Omit) => ( { - const { isDetails } = props; - - if (isDetails) { - return ; - } - - return ; +import { AllSummaryColumnProps } from '@kbn/discover-contextual-components'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; + +const LazySummaryColumn = dynamic( + () => + import( + '@kbn/discover-contextual-components/src/data_types/logs/components/summary_column/summary_column' + ) +); + +export const SummaryColumn = (props: Omit) => { + const { share, core } = useDiscoverServices(); + return ; }; - -// eslint-disable-next-line import/no-default-export -export default SummaryColumn; - -const SummaryCell = ({ - density: maybeNullishDensity, - rowHeight: maybeNullishRowHeight, - ...props -}: SummaryColumnProps & SummaryColumnFactoryDeps) => { - const { onFilter, row } = props; - - const density = maybeNullishDensity ?? DataGridDensity.COMPACT; - const isCompressed = density === DataGridDensity.COMPACT; - - const rowHeight = maybeNullishRowHeight ?? ROWS_HEIGHT_OPTIONS.single; - const isSingleLine = rowHeight === ROWS_HEIGHT_OPTIONS.single || rowHeight === 1; - - const resourceFields = createResourceFields(row); - const shouldRenderResource = resourceFields.length > 0; - - return isSingleLine ? ( - - {shouldRenderResource && ( - - )} - - - ) : ( - <> - {shouldRenderResource && ( - - )} - - - ); -}; - -const SummaryCellPopover = (props: SummaryColumnProps & SummaryColumnFactoryDeps) => { - const { row, dataView, fieldFormats, onFilter, closePopover } = props; - - const resourceFields = createResourceFields(row); - const shouldRenderResource = resourceFields.length > 0; - - const documentOverview = getLogDocumentOverview(row, { dataView, fieldFormats }); - const { field, value } = getMessageFieldWithFallbacks(documentOverview); - const shouldRenderContent = Boolean(field && value); - - const shouldRenderSource = !shouldRenderContent; - - return ( - - - {shouldRenderResource && ( - - - {resourceLabel} - - - - )} - - - {contentLabel} - - {shouldRenderContent && ( - - - {field} - - - {value} - - - )} - {shouldRenderSource && ( - - - {jsonLabel} - - - - )} - - - ); -}; - -const singleLineResourceCss = { - flexGrow: 0, - lineHeight: 'normal', - marginTop: -1, -}; - -const multiLineResourceCss = { display: 'inline-flex' }; diff --git a/src/plugins/discover/public/components/data_types/logs/summary_column/utils.tsx b/src/plugins/discover/public/components/data_types/logs/summary_column/utils.tsx deleted file mode 100644 index 470ec8a0f86fa..0000000000000 --- a/src/plugins/discover/public/components/data_types/logs/summary_column/utils.tsx +++ /dev/null @@ -1,126 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { getFieldValue, LogDocument, ResourceFields } from '@kbn/discover-utils/src'; -import { DataTableRecord } from '@kbn/discover-utils'; -import { dynamic } from '@kbn/shared-ux-utility'; -import React from 'react'; -import { css } from '@emotion/react'; -import { AgentName } from '@kbn/elastic-agent-utils'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { getAvailableResourceFields } from '../../../../utils/get_available_resource_fields'; -import * as constants from '../../../../../common/data_types/logs/constants'; -import { ServiceNameBadgeWithActions } from '../service_name_badge_with_actions'; -import { FieldBadgeWithActions, FieldBadgeWithActionsProps } from '../cell_actions_popover'; - -/** - * getUnformattedResourceFields definitions - */ -export const getUnformattedResourceFields = (doc: LogDocument): ResourceFields => { - const serviceName = getFieldValue(doc, constants.SERVICE_NAME_FIELD); - const hostName = getFieldValue(doc, constants.HOST_NAME_FIELD); - const agentName = getFieldValue(doc, constants.AGENT_NAME_FIELD); - const orchestratorClusterName = getFieldValue(doc, constants.ORCHESTRATOR_CLUSTER_NAME_FIELD); - const orchestratorResourceId = getFieldValue(doc, constants.ORCHESTRATOR_RESOURCE_ID_FIELD); - const orchestratorNamespace = getFieldValue(doc, constants.ORCHESTRATOR_NAMESPACE_FIELD); - const containerName = getFieldValue(doc, constants.CONTAINER_NAME_FIELD); - const containerId = getFieldValue(doc, constants.CONTAINER_ID_FIELD); - const cloudInstanceId = getFieldValue(doc, constants.CLOUD_INSTANCE_ID_FIELD); - - return { - [constants.SERVICE_NAME_FIELD]: serviceName, - [constants.HOST_NAME_FIELD]: hostName, - [constants.AGENT_NAME_FIELD]: agentName, - [constants.ORCHESTRATOR_CLUSTER_NAME_FIELD]: orchestratorClusterName, - [constants.ORCHESTRATOR_RESOURCE_ID_FIELD]: orchestratorResourceId, - [constants.ORCHESTRATOR_NAMESPACE_FIELD]: orchestratorNamespace, - [constants.CONTAINER_NAME_FIELD]: containerName, - [constants.CONTAINER_ID_FIELD]: containerId, - [constants.CLOUD_INSTANCE_ID_FIELD]: cloudInstanceId, - }; -}; - -/** - * createResourceFields definitions - */ -const AgentIcon = dynamic(() => import('@kbn/custom-icons/src/components/agent_icon')); - -const resourceCustomComponentsMap: Partial< - Record> -> = { - [constants.SERVICE_NAME_FIELD]: ServiceNameBadgeWithActions, -}; - -export interface ResourceFieldDescriptor { - ResourceBadge: React.ComponentType; - Icon?: () => JSX.Element; - name: keyof ResourceFields; - value: string; -} - -export const createResourceFields = (row: DataTableRecord): ResourceFieldDescriptor[] => { - const resourceDoc = getUnformattedResourceFields(row as LogDocument); - - const availableResourceFields = getAvailableResourceFields(resourceDoc); - - const resourceFields = availableResourceFields.map((name) => ({ - name, - value: resourceDoc[name] as string, - ResourceBadge: resourceCustomComponentsMap[name] ?? FieldBadgeWithActions, - ...(name === constants.SERVICE_NAME_FIELD && { - Icon: () => ( - - ), - }), - })); - - return resourceFields; -}; - -/** - * formatJsonDocumentForContent definitions - */ -export const formatJsonDocumentForContent = (row: DataTableRecord) => { - const flattenedResult: DataTableRecord['flattened'] = {}; - const rawFieldResult: DataTableRecord['raw']['fields'] = {}; - const { raw, flattened } = row; - const { fields } = raw; - - // We need 2 loops here for flattened and raw.fields. Flattened contains all fields, - // whereas raw.fields only contains certain fields excluding _ignored - for (const fieldName in flattened) { - if (isFieldAllowed(fieldName) && flattened[fieldName]) { - flattenedResult[fieldName] = flattened[fieldName]; - } - } - - for (const fieldName in fields) { - if (isFieldAllowed(fieldName) && fields[fieldName]) { - rawFieldResult[fieldName] = fields[fieldName]; - } - } - - return { - ...row, - flattened: flattenedResult, - raw: { - ...raw, - fields: rawFieldResult, - }, - }; -}; - -const isFieldAllowed = (field: string) => - !constants.FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT.some((prefix) => field.startsWith(prefix)); diff --git a/src/plugins/discover/public/components/data_types/logs/translations.tsx b/src/plugins/discover/public/components/data_types/logs/translations.tsx deleted file mode 100644 index bbc39022bd503..0000000000000 --- a/src/plugins/discover/public/components/data_types/logs/translations.tsx +++ /dev/null @@ -1,305 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiCode } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -export const flyoutContentLabel = i18n.translate('discover.logs.flyoutDetail.label.message', { - defaultMessage: 'Content breakdown', -}); - -export const jsonLabel = i18n.translate('discover.logs.dataTable.header.popover.json', { - defaultMessage: 'JSON', -}); - -export const contentLabel = i18n.translate('discover.logs.dataTable.header.popover.content', { - defaultMessage: 'Content', -}); - -export const resourceLabel = i18n.translate('discover.logs.dataTable.header.popover.resource', { - defaultMessage: 'Resource', -}); - -export const actionsLabel = i18n.translate('discover.logs.dataTable.header.popover.actions', { - defaultMessage: 'Actions', -}); - -export const actionsLabelLowerCase = i18n.translate( - 'discover.logs.dataTable.header.popover.actions.lowercase', - { - defaultMessage: 'actions', - } -); - -export const flyoutServiceLabel = i18n.translate('discover.logs.flyoutDetail.label.service', { - defaultMessage: 'Service', -}); - -export const flyoutTraceLabel = i18n.translate('discover.logs.flyoutDetail.label.trace', { - defaultMessage: 'Trace', -}); - -export const flyoutHostNameLabel = i18n.translate('discover.logs.flyoutDetail.label.hostName', { - defaultMessage: 'Host name', -}); - -export const serviceInfraAccordionTitle = i18n.translate( - 'discover.logs.flyoutDetail.accordion.title.serviceInfra', - { - defaultMessage: 'Service & Infrastructure', - } -); - -export const cloudAccordionTitle = i18n.translate( - 'discover.logs.flyoutDetail.accordion.title.cloud', - { - defaultMessage: 'Cloud', - } -); - -export const otherAccordionTitle = i18n.translate( - 'discover.logs.flyoutDetail.accordion.title.other', - { - defaultMessage: 'Other', - } -); - -export const flyoutOrchestratorClusterNameLabel = i18n.translate( - 'discover.logs.flyoutDetail.label.orchestratorClusterName', - { - defaultMessage: 'Orchestrator cluster Name', - } -); - -export const flyoutOrchestratorResourceIdLabel = i18n.translate( - 'discover.logs.flyoutDetail.label.orchestratorResourceId', - { - defaultMessage: 'Orchestrator resource ID', - } -); - -export const flyoutCloudProviderLabel = i18n.translate( - 'discover.logs.flyoutDetail.label.cloudProvider', - { - defaultMessage: 'Cloud provider', - } -); - -export const flyoutCloudRegionLabel = i18n.translate( - 'discover.logs.flyoutDetail.label.cloudRegion', - { - defaultMessage: 'Cloud region', - } -); - -export const flyoutCloudAvailabilityZoneLabel = i18n.translate( - 'discover.logs.flyoutDetail.label.cloudAvailabilityZone', - { - defaultMessage: 'Cloud availability zone', - } -); - -export const flyoutCloudProjectIdLabel = i18n.translate( - 'discover.logs.flyoutDetail.label.cloudProjectId', - { - defaultMessage: 'Cloud project ID', - } -); - -export const flyoutCloudInstanceIdLabel = i18n.translate( - 'discover.logs.flyoutDetail.label.cloudInstanceId', - { - defaultMessage: 'Cloud instance ID', - } -); - -export const flyoutLogPathFileLabel = i18n.translate( - 'discover.logs.flyoutDetail.label.logPathFile', - { - defaultMessage: 'Log path file', - } -); - -export const flyoutNamespaceLabel = i18n.translate('discover.logs.flyoutDetail.label.namespace', { - defaultMessage: 'Namespace', -}); - -export const flyoutDatasetLabel = i18n.translate('discover.logs.flyoutDetail.label.dataset', { - defaultMessage: 'Dataset', -}); - -export const flyoutShipperLabel = i18n.translate('discover.logs.flyoutDetail.label.shipper', { - defaultMessage: 'Shipper', -}); - -export const actionFilterForText = (text: string) => - i18n.translate('discover.logs.flyoutDetail.value.hover.filterFor', { - defaultMessage: 'Filter for this {value}', - values: { - value: text, - }, - }); - -export const actionFilterOutText = (text: string) => - i18n.translate('discover.logs.flyoutDetail.value.hover.filterOut', { - defaultMessage: 'Filter out this {value}', - values: { - value: text, - }, - }); - -export const filterOutText = i18n.translate('discover.logs.popoverAction.filterOut', { - defaultMessage: 'Filter out', -}); - -export const filterForText = i18n.translate('discover.logs.popoverAction.filterFor', { - defaultMessage: 'Filter for', -}); - -export const flyoutHoverActionFilterForFieldPresentText = i18n.translate( - 'discover.logs.flyoutDetail.value.hover.filterForFieldPresent', - { - defaultMessage: 'Filter for field present', - } -); - -export const flyoutHoverActionToggleColumnText = i18n.translate( - 'discover.logs.flyoutDetail.value.hover.toggleColumn', - { - defaultMessage: 'Toggle column in table', - } -); - -export const flyoutHoverActionCopyToClipboardText = i18n.translate( - 'discover.logs.flyoutDetail.value.hover.copyToClipboard', - { - defaultMessage: 'Copy to clipboard', - } -); - -export const copyValueText = i18n.translate('discover.logs.popoverAction.copyValue', { - defaultMessage: 'Copy value', -}); - -export const copyValueAriaText = (fieldName: string) => - i18n.translate('discover.logs.popoverAction.copyValueAriaText', { - defaultMessage: 'Copy value of {fieldName}', - values: { - fieldName, - }, - }); - -export const flyoutAccordionShowMoreText = (count: number) => - i18n.translate('discover.logs.flyoutDetail.section.showMore', { - defaultMessage: '+ {hiddenCount} more', - values: { - hiddenCount: count, - }, - }); - -export const openCellActionPopoverAriaText = i18n.translate( - 'discover.logs.popoverAction.openPopover', - { - defaultMessage: 'Open popover', - } -); - -export const closeCellActionPopoverText = i18n.translate( - 'discover.logs.popoverAction.closePopover', - { - defaultMessage: 'Close popover', - } -); - -export const contentHeaderTooltipParagraph1 = ( - log.level, - message: message, - }} - /> -); - -export const contentHeaderTooltipParagraph2 = i18n.translate( - 'discover.logs.dataTable.header.content.tooltip.paragraph2', - { - defaultMessage: 'When the message field is empty, one of the following is displayed:', - } -); - -export const resourceHeaderTooltipParagraph = i18n.translate( - 'discover.logs.dataTable.header.resource.tooltip.paragraph', - { - defaultMessage: "Fields that provide information on the document's source, such as:", - } -); - -export const actionsHeaderTooltipParagraph = i18n.translate( - 'discover.logs.dataTable.header.actions.tooltip.paragraph', - { - defaultMessage: 'Fields that provide actionable information, such as:', - } -); - -export const actionsHeaderTooltipExpandAction = i18n.translate( - 'discover.logs.dataTable.header.actions.tooltip.expand', - { defaultMessage: 'Expand log details' } -); - -export const actionsHeaderTooltipDegradedAction = ( - - _ignored - - ), - }} - /> -); - -export const actionsHeaderTooltipStacktraceAction = i18n.translate( - 'discover.logs.dataTable.header.actions.tooltip.stacktrace', - { defaultMessage: 'Access to available stacktraces based on:' } -); - -export const degradedDocButtonLabelWhenPresent = i18n.translate( - 'discover.logs.dataTable.controlColumn.actions.button.degradedDocPresent', - { - defaultMessage: - "This document couldn't be parsed correctly. Not all fields are properly populated", - } -); - -export const degradedDocButtonLabelWhenNotPresent = i18n.translate( - 'discover.logs.dataTable.controlColumn.actions.button.degradedDocNotPresent', - { - defaultMessage: 'All fields in this document were parsed correctly', - } -); - -export const stacktraceAvailableControlButton = i18n.translate( - 'discover.logs.dataTable.controlColumn.actions.button.stacktrace.available', - { - defaultMessage: 'Stacktraces available', - } -); - -export const stacktraceNotAvailableControlButton = i18n.translate( - 'discover.logs.dataTable.controlColumn.actions.button.stacktrace.notAvailable', - { - defaultMessage: 'Stacktraces not available', - } -); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx b/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx index 7e13baf8ddcf9..68349aeefa09a 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx @@ -8,12 +8,12 @@ */ import { SOURCE_COLUMN } from '@kbn/unified-data-table'; +import { getLogLevelBadgeCell } from '@kbn/discover-contextual-components'; import { getSummaryColumn } from '../../../../../components/data_types/logs/summary_column'; import { LOG_LEVEL_FIELDS, SERVICE_NAME_FIELDS, } from '../../../../../../common/data_types/logs/constants'; -import { getLogLevelBadgeCell } from '../../../../../components/data_types/logs/log_level_badge_cell'; import { getServiceNameCell } from '../../../../../components/data_types/logs/service_name_cell'; import type { DataSourceProfileProvider } from '../../../../profiles'; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 1f3ed529d804b..197d323d7d221 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -96,11 +96,9 @@ "@kbn/observability-ai-assistant-plugin", "@kbn/fields-metadata-plugin", "@kbn/security-solution-common", - "@kbn/router-utils", - "@kbn/management-settings-ids", - "@kbn/react-hooks", "@kbn/logs-data-access-plugin", "@kbn/core-lifecycle-browser", + "@kbn/discover-contextual-components", "@kbn/esql-ast" ], "exclude": [ diff --git a/src/plugins/unified_doc_viewer/kibana.jsonc b/src/plugins/unified_doc_viewer/kibana.jsonc index 56ea8951e3a2d..6bd1b738c0ccb 100644 --- a/src/plugins/unified_doc_viewer/kibana.jsonc +++ b/src/plugins/unified_doc_viewer/kibana.jsonc @@ -12,3 +12,4 @@ "optionalPlugins": ["fieldsMetadata"] } } + \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index e2d84fa57311c..2da0c007278e8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -764,6 +764,8 @@ "@kbn/dev-utils/*": ["packages/kbn-dev-utils/*"], "@kbn/developer-examples-plugin": ["examples/developer_examples"], "@kbn/developer-examples-plugin/*": ["examples/developer_examples/*"], + "@kbn/discover-contextual-components": ["packages/kbn-discover-contextual-components"], + "@kbn/discover-contextual-components/*": ["packages/kbn-discover-contextual-components/*"], "@kbn/discover-customization-examples-plugin": ["examples/discover_customization_examples"], "@kbn/discover-customization-examples-plugin/*": ["examples/discover_customization_examples/*"], "@kbn/discover-enhanced-plugin": ["x-pack/plugins/discover_enhanced"], diff --git a/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx b/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx index fe108289985a9..676468c2f4a32 100644 --- a/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx +++ b/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx @@ -13,11 +13,17 @@ import { i18n } from '@kbn/i18n'; import { getRouterLinkProps } from '@kbn/router-utils'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import React, { useCallback, useMemo } from 'react'; -import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source'; +import type { ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source'; +interface LinkFilter { + filter: QueryDslQueryContainer; + meta?: { + name?: string; + }; +} export interface DiscoverLinkProps { - documentFilters?: QueryDslQueryContainer[]; - logsSource: IndexNameLogsSourceConfiguration; + documentFilters?: LinkFilter[]; + logsSource: ResolvedIndexNameLogsSourceConfiguration; timeRange: { start: string; end: string; @@ -46,10 +52,10 @@ export const DiscoverLink = React.memo( filters: documentFilters?.map((filter) => buildCustomFilter( logsSource.indexName, - filter, + filter.filter, false, false, - categorizedLogsFilterLabel, + filter.meta?.name ?? categorizedLogsFilterLabel, FilterStateStore.APP_STATE ) ), diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx index 6204667827281..1ce45ca6b3727 100644 --- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx @@ -14,7 +14,12 @@ import { categorizeLogsService, createCategorizeLogsServiceImplementations, } from '../../services/categorize_logs_service'; -import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source'; +import { + categoryDetailsService, + createCategoryDetailsServiceImplementations, +} from '../../services/category_details_service'; +import { LogCategory } from '../../types'; +import { ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source'; import { LogCategoriesErrorContent } from './log_categories_error_content'; import { LogCategoriesLoadingContent } from './log_categories_loading_content'; import { @@ -25,7 +30,7 @@ import { export interface LogCategoriesProps { dependencies: LogCategoriesDependencies; documentFilters?: QueryDslQueryContainer[]; - logsSource: IndexNameLogsSourceConfiguration; + logsSource: ResolvedIndexNameLogsSourceConfiguration; // The time range could be made optional if we want to support an internal // time range picker timeRange: { @@ -61,12 +66,49 @@ export const LogCategories: React.FC = ({ } ); + const [categoryDetailsServiceState, sendToCategoryDetailsService] = useMachine( + categoryDetailsService.provide( + createCategoryDetailsServiceImplementations({ search: dependencies.search }) + ), + { + inspect: consoleInspector, + input: { + index: logsSource.indexName, + startTimestamp: timeRange.start, + endTimestamp: timeRange.end, + timeField: logsSource.timestampField, + messageField: logsSource.messageField, + additionalFilters: documentFilters, + dataView: logsSource.dataView, + }, + } + ); + const cancelOperation = useCallback(() => { sendToCategorizeLogsService({ type: 'cancel', }); }, [sendToCategorizeLogsService]); + const closeFlyout = useCallback(() => { + sendToCategoryDetailsService({ + type: 'setExpandedCategory', + category: null, + rowIndex: null, + }); + }, [sendToCategoryDetailsService]); + + const openFlyout = useCallback( + (category: LogCategory | null, rowIndex: number | null) => { + sendToCategoryDetailsService({ + type: 'setExpandedCategory', + category, + rowIndex, + }); + }, + [sendToCategoryDetailsService] + ); + if (categorizeLogsServiceState.matches('done')) { return ( = ({ logCategories={categorizeLogsServiceState.context.categories} logsSource={logsSource} timeRange={timeRange} + categoryDetailsServiceState={categoryDetailsServiceState} + onCloseFlyout={closeFlyout} + onOpenFlyout={openFlyout} /> ); } else if (categorizeLogsServiceState.matches('failed')) { diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx index 4538b0ec2fd5d..7c13ac1446320 100644 --- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx @@ -8,13 +8,13 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { SharePluginStart } from '@kbn/share-plugin/public'; -import React from 'react'; -import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source'; +import React, { useMemo } from 'react'; +import type { ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source'; import { DiscoverLink } from '../discover_link'; export interface LogCategoriesControlBarProps { documentFilters?: QueryDslQueryContainer[]; - logsSource: IndexNameLogsSourceConfiguration; + logsSource: ResolvedIndexNameLogsSourceConfiguration; timeRange: { start: string; end: string; @@ -28,12 +28,17 @@ export interface LogCategoriesControlBarDependencies { export const LogCategoriesControlBar: React.FC = React.memo( ({ dependencies, documentFilters, logsSource, timeRange }) => { + const linkFilters = useMemo( + () => documentFilters?.map((filter) => ({ filter })), + [documentFilters] + ); + return ( diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx index d9e960685de99..badd316371ec6 100644 --- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx @@ -25,10 +25,14 @@ import { logCategoriesGridColumns, renderLogCategoriesGridCell, } from './log_categories_grid_cell'; +import { createLogCategoriesGridControlColumns } from './log_categories_grid_control_columns'; export interface LogCategoriesGridProps { dependencies: LogCategoriesGridDependencies; logCategories: LogCategory[]; + expandedRowIndex: number | null; + onOpenFlyout: (category: LogCategory, rowIndex: number) => void; + onCloseFlyout: () => void; } export type LogCategoriesGridDependencies = LogCategoriesGridCellDependencies; @@ -36,6 +40,9 @@ export type LogCategoriesGridDependencies = LogCategoriesGridCellDependencies; export const LogCategoriesGrid: React.FC = ({ dependencies, logCategories, + expandedRowIndex, + onOpenFlyout, + onCloseFlyout, }) => { const [gridState, dispatchGridEvent] = useMachine(gridStateService, { input: { @@ -93,6 +100,11 @@ export const LogCategoriesGrid: React.FC = ({ onSort: (sortingColumns) => dispatchGridEvent({ type: 'changeSortingColumns', sortingColumns }), }} + leadingControlColumns={createLogCategoriesGridControlColumns({ + expandedRowIndex, + onOpenFlyout, + onCloseFlyout, + })} /> ); }; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx index d6ab4969eaf7b..7e40d192df227 100644 --- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx @@ -83,7 +83,7 @@ export type LogCategoriesGridColumnId = (typeof logCategoriesGridColumns)[number const cellContextKey = 'cellContext'; -const getCellContext = (cellContext: object): LogCategoriesGridCellContext => +export const getCellContext = (cellContext: object): LogCategoriesGridCellContext => (cellContextKey in cellContext ? cellContext[cellContextKey] : {}) as LogCategoriesGridCellContext; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_control_columns.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_control_columns.tsx new file mode 100644 index 0000000000000..546754ae1cc5b --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_control_columns.tsx @@ -0,0 +1,45 @@ +/* + * 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 { EuiScreenReaderOnly } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { LogCategory } from '../../types'; +import { createLogCategoriesGridExpandButton } from './log_categories_grid_expand_button'; + +const DEFAULT_CONTROL_COLUMN_WIDTH = 40; + +interface ControlColumnsProps { + expandedRowIndex: number | null; + onOpenFlyout: (category: LogCategory, rowIndex: number) => void; + onCloseFlyout: () => void; +} + +export const createLogCategoriesGridControlColumns = (props: ControlColumnsProps) => { + const { expandedRowIndex, onOpenFlyout, onCloseFlyout } = props; + + return [ + { + id: 'toggleFlyout', + width: DEFAULT_CONTROL_COLUMN_WIDTH, + headerCellRender: () => ( + + + {i18n.translate('xpack.observabilityLogsOverview.controlColumnHeader', { + defaultMessage: 'Control column', + })} + + + ), + rowCellRender: createLogCategoriesGridExpandButton({ + expandedRowIndex, + onOpenFlyout, + onCloseFlyout, + }), + }, + ]; +}; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_expand_button.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_expand_button.tsx new file mode 100644 index 0000000000000..34c8e72e2d91a --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_expand_button.tsx @@ -0,0 +1,71 @@ +/* + * 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 { EuiButtonIcon, EuiToolTip, RenderCellValue } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback } from 'react'; +import { LogCategory } from '../../types'; +import { getCellContext } from './log_categories_grid_cell'; + +interface CreateLogCategoriesGridExpandButtonProps { + expandedRowIndex: number | null; + onOpenFlyout: (category: LogCategory, rowIndex: number) => void; + onCloseFlyout: () => void; +} + +export const createLogCategoriesGridExpandButton = + ({ + expandedRowIndex, + onOpenFlyout, + onCloseFlyout, + }: CreateLogCategoriesGridExpandButtonProps): RenderCellValue => + (props) => { + const { rowIndex } = props; + const { logCategories } = getCellContext(props); + const logCategory = logCategories[rowIndex]; + const isCurrentRowExpanded = expandedRowIndex === rowIndex; + const onClickHandler = useCallback(() => { + if (isCurrentRowExpanded) { + onCloseFlyout(); + } else { + onOpenFlyout(logCategory, rowIndex); + } + }, [isCurrentRowExpanded, logCategory, rowIndex]); + + return ( + + ); + }; + +interface ExpandButtonProps { + isCurrentRowExpanded: boolean; + onClickHandler: () => void; +} + +const ExpandButton: React.FC = ({ isCurrentRowExpanded, onClickHandler }) => { + return ( + + + + ); +}; + +const buttonLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategoriesGrid.controlColumns.toggleFlyout', + { + defaultMessage: 'Toggle flyout with details', + } +); diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx index d507487a99e3c..7507ab5b23f44 100644 --- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { EuiDataGridColumn, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; +import { EuiDataGridColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; +import React from 'react'; import { LogCategory } from '../../types'; +import { LogCategoryPattern } from '../shared/log_category_pattern'; export const logCategoriesGridPatternColumn = { id: 'pattern' as const, @@ -27,34 +27,5 @@ export interface LogCategoriesGridPatternCellProps { export const LogCategoriesGridPatternCell: React.FC = ({ logCategory, }) => { - const theme = useEuiTheme(); - const { euiTheme } = theme; - const termsList = useMemo(() => logCategory.terms.split(' '), [logCategory.terms]); - - const commonStyle = css` - display: inline-block; - font-family: ${euiTheme.font.familyCode}; - margin-right: ${euiTheme.size.xs}; - `; - - const termStyle = css` - ${commonStyle}; - `; - - const separatorStyle = css` - ${commonStyle}; - color: ${euiTheme.colors.successText}; - `; - - return ( -
-      
*
- {termsList.map((term, index) => ( - -
{term}
-
*
-
- ))} -
- ); + return ; }; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx index e16bdda7cb44a..c2b1a0989c2ec 100644 --- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx @@ -9,8 +9,14 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/type import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { StateFrom } from 'xstate5'; +import { categoryDetailsService } from '../../services/category_details_service'; import { LogCategory } from '../../types'; -import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source'; +import { ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source'; +import { + LogCategoriesFlyoutDependencies, + LogCategoryDetailsFlyout, +} from '../log_category_details/log_category_details_flyout'; import { LogCategoriesControlBar, LogCategoriesControlBarDependencies, @@ -21,15 +27,19 @@ export interface LogCategoriesResultContentProps { dependencies: LogCategoriesResultContentDependencies; documentFilters?: QueryDslQueryContainer[]; logCategories: LogCategory[]; - logsSource: IndexNameLogsSourceConfiguration; + logsSource: ResolvedIndexNameLogsSourceConfiguration; timeRange: { start: string; end: string; }; + categoryDetailsServiceState: StateFrom; + onCloseFlyout: () => void; + onOpenFlyout: (category: LogCategory, rowIndex: number) => void; } export type LogCategoriesResultContentDependencies = LogCategoriesControlBarDependencies & - LogCategoriesGridDependencies; + LogCategoriesGridDependencies & + LogCategoriesFlyoutDependencies; export const LogCategoriesResultContent: React.FC = ({ dependencies, @@ -37,6 +47,9 @@ export const LogCategoriesResultContent: React.FC { if (logCategories.length === 0) { return ; @@ -52,7 +65,24 @@ export const LogCategoriesResultContent: React.FC
- + + {categoryDetailsServiceState.context.expandedCategory && ( + + )}
); diff --git a/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_error_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_error_content.tsx new file mode 100644 index 0000000000000..509d35b0068e5 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_error_content.tsx @@ -0,0 +1,41 @@ +/* + * 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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export interface LogCategoryDetailsErrorContentProps { + error?: Error; + title: string; +} + +export const LogCategoryDetailsErrorContent: React.FC = ({ + error, + title, +}) => { + return ( + {title}} + body={ + +

{error?.stack ?? error?.toString() ?? unknownErrorDescription}

+
+ } + layout="vertical" + /> + ); +}; + +const unknownErrorDescription = i18n.translate( + 'xpack.observabilityLogsOverview.logCategoryDetails.unknownErrorDescription', + { + defaultMessage: 'An unspecified error occurred.', + } +); diff --git a/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_flyout.tsx b/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_flyout.tsx new file mode 100644 index 0000000000000..2f478c771dbfa --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_flyout.tsx @@ -0,0 +1,139 @@ +/* + * 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, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + useGeneratedHtmlId, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { StateFrom } from 'xstate5'; +import { i18n } from '@kbn/i18n'; +import { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; +import { LogCategory } from '../../types'; +import { LogCategoryPattern } from '../shared/log_category_pattern'; +import { categoryDetailsService } from '../../services/category_details_service'; +import { + LogCategoryDocumentExamplesTable, + LogCategoryDocumentExamplesTableDependencies, +} from './log_category_document_examples_table'; +import { type ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source'; +import { LogCategoryDetailsLoadingContent } from './log_category_details_loading_content'; +import { LogCategoryDetailsErrorContent } from './log_category_details_error_content'; +import { DiscoverLink } from '../discover_link'; +import { createCategoryQuery } from '../../services/categorize_logs_service/queries'; + +export type LogCategoriesFlyoutDependencies = LogCategoryDocumentExamplesTableDependencies; + +interface LogCategoryDetailsFlyoutProps { + onCloseFlyout: () => void; + logCategory: LogCategory; + categoryDetailsServiceState: StateFrom; + dependencies: LogCategoriesFlyoutDependencies; + logsSource: ResolvedIndexNameLogsSourceConfiguration; + documentFilters?: QueryDslQueryContainer[]; + timeRange: { + start: string; + end: string; + }; +} + +export const LogCategoryDetailsFlyout: React.FC = ({ + onCloseFlyout, + logCategory, + categoryDetailsServiceState, + dependencies, + logsSource, + documentFilters, + timeRange, +}) => { + const flyoutTitleId = useGeneratedHtmlId({ + prefix: 'flyoutTitle', + }); + + const linkFilters = useMemo(() => { + return [ + ...(documentFilters ? documentFilters.map((filter) => ({ filter })) : []), + { + filter: createCategoryQuery(logsSource.messageField)(logCategory.terms), + meta: { + name: i18n.translate( + 'xpack.observabilityLogsOverview.logCategoryDetailsFlyout.discoverLinkFilterName', + { + defaultMessage: 'Category: {terms}', + values: { + terms: logCategory.terms, + }, + } + ), + }, + }, + ]; + }, [documentFilters, logCategory.terms, logsSource.messageField]); + + return ( + onCloseFlyout()} aria-labelledby={flyoutTitleId}> + + + + +

+ +

+
+ + +
+ + + +
+
+ + {categoryDetailsServiceState.matches({ hasCategory: 'fetchingDocuments' }) ? ( + + ) : categoryDetailsServiceState.matches({ hasCategory: 'error' }) ? ( + + ) : ( + + )} + +
+ ); +}; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_loading_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_loading_content.tsx new file mode 100644 index 0000000000000..fd6aa50a38221 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_details_loading_content.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import React from 'react'; + +interface LogCategoryDetailsLoadingContentProps { + message: string; +} + +export const LogCategoryDetailsLoadingContent: React.FC = ({ + message, +}) => { + return } title={

{message}

} />; +}; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_document_examples_table.tsx b/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_document_examples_table.tsx new file mode 100644 index 0000000000000..6b43fa86fe49e --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_category_details/log_category_document_examples_table.tsx @@ -0,0 +1,151 @@ +/* + * 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 { EuiBasicTable, EuiBasicTableColumn, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { DataGridDensity, ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table'; +import moment from 'moment'; +import type { SettingsStart } from '@kbn/core-ui-settings-browser'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { getLogLevelBadgeCell, LazySummaryColumn } from '@kbn/discover-contextual-components'; +import type { LogCategoryDocument } from '../../services/category_details_service/types'; +import { type ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source'; + +export interface LogCategoryDocumentExamplesTableDependencies { + core: CoreStart; + uiSettings: SettingsStart; + fieldFormats: FieldFormatsStart; + share: SharePluginStart; +} + +export interface LogCategoryDocumentExamplesTableProps { + dependencies: LogCategoryDocumentExamplesTableDependencies; + categoryDocuments: LogCategoryDocument[]; + logsSource: ResolvedIndexNameLogsSourceConfiguration; +} + +const TimestampCell = ({ + dependencies, + timestamp, +}: { + dependencies: LogCategoryDocumentExamplesTableDependencies; + timestamp?: string | number; +}) => { + const dateFormat = useMemo( + () => dependencies.uiSettings.client.get('dateFormat'), + [dependencies.uiSettings.client] + ); + if (!timestamp) return null; + + if (dateFormat) { + return <>{moment(timestamp).format(dateFormat)}; + } else { + return <>{timestamp}; + } +}; + +const LogLevelBadgeCell = getLogLevelBadgeCell('log.level'); + +export const LogCategoryDocumentExamplesTable: React.FC = ({ + categoryDocuments, + dependencies, + logsSource, +}) => { + const columns: Array> = [ + { + field: 'row', + name: 'Timestamp', + width: '25%', + render: (row: any) => { + return ( + + ); + }, + }, + { + field: 'row', + name: 'Log level', + width: '10%', + render: (row: any) => { + return ( + {}} + closePopover={() => {}} + /> + ); + }, + }, + { + field: 'row', + name: 'Summary', + width: '65%', + render: (row: any) => { + return ( + {}} + closePopover={() => {}} + density={DataGridDensity.COMPACT} + rowHeight={ROWS_HEIGHT_OPTIONS.single} + shouldShowFieldHandler={() => false} + core={dependencies.core} + share={dependencies.share} + /> + ); + }, + }, + ]; + return ( + <> + + {i18n.translate( + 'xpack.observabilityLogsOverview.logCategoryDocumentExamplesTable.documentCountText', + { + defaultMessage: 'Displaying the latest {documentsCount} documents.', + values: { + documentsCount: categoryDocuments.length, + }, + } + )} + + + + + ); +}; diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx index 988656eb1571e..77535228f7af6 100644 --- a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx +++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx @@ -9,6 +9,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { type LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; import React from 'react'; import useAsync from 'react-use/lib/useAsync'; +import { DataViewsContract } from '@kbn/data-views-plugin/public'; import { LogsSourceConfiguration, normalizeLogsSource } from '../../utils/logs_source'; import { LogCategories, LogCategoriesDependencies } from '../log_categories'; import { LogsOverviewErrorContent } from './logs_overview_error_content'; @@ -26,6 +27,7 @@ export interface LogsOverviewProps { export type LogsOverviewDependencies = LogCategoriesDependencies & { logsDataAccess: LogsDataAccessPluginStart; + dataViews: DataViewsContract; }; export const LogsOverview: React.FC = React.memo( @@ -36,8 +38,12 @@ export const LogsOverview: React.FC = React.memo( timeRange, }) => { const normalizedLogsSource = useAsync( - () => normalizeLogsSource({ logsDataAccess: dependencies.logsDataAccess })(logsSource), - [dependencies.logsDataAccess, logsSource] + () => + normalizeLogsSource({ + logsDataAccess: dependencies.logsDataAccess, + dataViewsService: dependencies.dataViews, + })(logsSource), + [dependencies.dataViews, dependencies.logsDataAccess, logsSource] ); if (normalizedLogsSource.loading) { diff --git a/x-pack/packages/observability/logs_overview/src/components/shared/log_category_pattern.tsx b/x-pack/packages/observability/logs_overview/src/components/shared/log_category_pattern.tsx new file mode 100644 index 0000000000000..8a8deb5918324 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/shared/log_category_pattern.tsx @@ -0,0 +1,50 @@ +/* + * 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 { useEuiTheme } from '@elastic/eui'; +import { useMemo } from 'react'; +import { css } from '@emotion/react'; +import React from 'react'; +import { getLogCategoryTerms } from '../../utils/log_category'; +import { LogCategory } from '../../types'; + +interface LogCategoryPatternProps { + logCategory: LogCategory; +} + +export const LogCategoryPattern: React.FC = ({ logCategory }) => { + const theme = useEuiTheme(); + const { euiTheme } = theme; + const termsList = useMemo(() => getLogCategoryTerms(logCategory), [logCategory]); + + const commonStyle = css` + display: inline-block; + font-family: ${euiTheme.font.familyCode}; + margin-right: ${euiTheme.size.xs}; + `; + + const termStyle = css` + ${commonStyle}; + `; + + const separatorStyle = css` + ${commonStyle}; + color: ${euiTheme.colors.successText}; + `; + + return ( +
+      
*
+ {termsList.map((term, index) => ( + +
{term}
+
*
+
+ ))} +
+ ); +}; diff --git a/x-pack/packages/observability/logs_overview/src/services/category_details_service/category_details_service.ts b/x-pack/packages/observability/logs_overview/src/services/category_details_service/category_details_service.ts new file mode 100644 index 0000000000000..958f717548600 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/category_details_service/category_details_service.ts @@ -0,0 +1,191 @@ +/* + * 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 { MachineImplementationsFrom, assign, setup } from 'xstate5'; +import { LogCategory } from '../../types'; +import { getPlaceholderFor } from '../../utils/xstate5_utils'; +import { + CategoryDetailsServiceDependencies, + LogCategoryDocument, + LogCategoryDetailsParams, +} from './types'; +import { getCategoryDocuments } from './category_documents'; + +export const categoryDetailsService = setup({ + types: { + input: {} as LogCategoryDetailsParams, + output: {} as { + categoryDocuments: LogCategoryDocument[] | null; + }, + context: {} as { + parameters: LogCategoryDetailsParams; + error?: Error; + expandedRowIndex: number | null; + expandedCategory: LogCategory | null; + categoryDocuments: LogCategoryDocument[]; + }, + events: {} as + | { + type: 'cancel'; + } + | { + type: 'setExpandedCategory'; + rowIndex: number | null; + category: LogCategory | null; + }, + }, + actors: { + getCategoryDocuments: getPlaceholderFor(getCategoryDocuments), + }, + actions: { + storeCategory: assign( + ({ context, event }, params: { category: LogCategory | null; rowIndex: number | null }) => ({ + expandedCategory: params.category, + expandedRowIndex: params.rowIndex, + }) + ), + storeDocuments: assign( + ({ context, event }, params: { categoryDocuments: LogCategoryDocument[] }) => ({ + categoryDocuments: params.categoryDocuments, + }) + ), + storeError: assign((_, params: { error: unknown }) => ({ + error: params.error instanceof Error ? params.error : new Error(String(params.error)), + })), + }, + guards: { + hasCategory: (_guardArgs, params: { expandedCategory: LogCategory | null }) => + params.expandedCategory !== null, + hasDocumentExamples: ( + _guardArgs, + params: { categoryDocuments: LogCategoryDocument[] | null } + ) => params.categoryDocuments !== null && params.categoryDocuments.length > 0, + }, +}).createMachine({ + /** @xstate-layout N4IgpgJg5mDOIC5QGMCGAXMUD2AnAlgF5gAy2UsAdMtgK4B26+9UAItsrQLZiOwDEEbPTCVmAN2wBrUWkw4CxMhWp1GzNh2690sBBI4Z8wgNoAGALrmLiUAAdssfE2G2QAD0QBmMwA5KACy+AQFmob4AjABMwQBsADQgAJ6IkYEAnJkA7FmxZlERmQGxAL4liXJYeESk5FQ0DEws7Jw8fILCogYy1BhVirUqDerNWm26+vSScsb01iYRNkggDk4u9G6eCD7+QSFhftFxiSkIvgCsWZSxEVlRsbFZ52Zm515lFX0KNcr1ak2aVo6ARCERiKbSWRfapKOqqRoaFraPiTaZGUyWExRJb2RzOWabbx+QLBULhI7FE7eWL+F45GnRPIRZkfECVb6wob-RFjYH8MC4XB4Sh2AA2GAAZnguL15DDBn8EaMgSiDDMMVZLG5VvjXMstjsSftyTFKclEOdzgFKF5zukvA8zBFnl50udWez5b94SNAcjdPw0PRkGBRdZtXj1oTtsS9mTDqaEuaEBF8udKFkIr5fK6olkzOksgEPdCBt6JWB0MgABYaADKqC4YsgAGFS-g4B0wd0oXKBg2m6LW+24OHljqo-rEMzbpQos8-K7fC9CknTrF0rEbbb0oVMoWIgF3eU2e3OVQK1XaywB82IG2+x2BAKhbgReL0FLcDLPf3G3eH36J8x1xNYCSnFNmSuecXhzdJlydTcqQQLJfHSOc0PyLJN3SMxYiPEtH3PShLxret-yHe8RwEIMQzDLVx0jcDQC2GdoIXOCENXZDsyiOcAiiKJ0iiPDLi8V1CKA4jSOvKAACUwC4VBmA0QDvk7UEughHpfxqBSlJUlg1OqUcGNA3UNggrMs347IjzdaIvGQwSvECXI8k3Z43gEiJJI5BUSMrMiWH05T6FU6j+UFYUxUlaVZSksBQsMqBjIIUycRWJi9RY6dIn8KIAjsu1zkc5CAmiG1fBiaIzB8B0QmPT4iICmSNGS8KjMi2jQxArKwJyjw8pswriocqInOTLwIi3ASD1yQpswCd5WXobAIDgNxdPPCMBss3KEAAWjXRBDvTfcLsu9Jlr8r04WGAEkXGeBGL26MBOQzIt2ut4cwmirCt8W6yzhNqbwo4dH0216LOjTMIjnBdYhK1DYgdHjihtZbUIdWIXJuYGflBoLZI6iKoZe8zJwOw9KtGt1kbuTcsmQrwi0oeCQjzZ5blwt1Cek5TKN22GIIKZbAgKC45pyLyeLwtz4Kyabs1QgWAs0kXqaGhBxdcnzpaE2XXmch0MORmaBJeLwjbKMogA */ + id: 'logCategoryDetails', + context: ({ input }) => ({ + expandedCategory: null, + expandedRowIndex: null, + categoryDocuments: [], + parameters: input, + }), + initial: 'idle', + states: { + idle: { + on: { + setExpandedCategory: { + target: 'checkingCategoryState', + actions: [ + { + type: 'storeCategory', + params: ({ event }) => event, + }, + ], + }, + }, + }, + checkingCategoryState: { + always: [ + { + guard: { + type: 'hasCategory', + params: ({ event, context }) => { + return { + expandedCategory: context.expandedCategory, + }; + }, + }, + target: '#hasCategory.fetchingDocuments', + }, + { target: 'idle' }, + ], + }, + hasCategory: { + id: 'hasCategory', + initial: 'fetchingDocuments', + on: { + setExpandedCategory: { + target: 'checkingCategoryState', + actions: [ + { + type: 'storeCategory', + params: ({ event }) => event, + }, + ], + }, + }, + states: { + fetchingDocuments: { + invoke: { + src: 'getCategoryDocuments', + id: 'fetchCategoryDocumentExamples', + input: ({ context }) => ({ + ...context.parameters, + categoryTerms: context.expandedCategory!.terms, + }), + onDone: [ + { + guard: { + type: 'hasDocumentExamples', + params: ({ event }) => { + return event.output; + }, + }, + target: 'hasData', + actions: [ + { + type: 'storeDocuments', + params: ({ event }) => { + return event.output; + }, + }, + ], + }, + { + target: 'noData', + actions: [ + { + type: 'storeDocuments', + params: ({ event }) => { + return { categoryDocuments: [] }; + }, + }, + ], + }, + ], + onError: { + target: 'error', + actions: [ + { + type: 'storeError', + params: ({ event }) => ({ error: event.error }), + }, + ], + }, + }, + }, + hasData: {}, + noData: {}, + error: {}, + }, + }, + }, + output: ({ context }) => ({ + categoryDocuments: context.categoryDocuments, + }), +}); + +export const createCategoryDetailsServiceImplementations = ({ + search, +}: CategoryDetailsServiceDependencies): MachineImplementationsFrom< + typeof categoryDetailsService +> => ({ + actors: { + getCategoryDocuments: getCategoryDocuments({ search }), + }, +}); diff --git a/x-pack/packages/observability/logs_overview/src/services/category_details_service/category_documents.ts b/x-pack/packages/observability/logs_overview/src/services/category_details_service/category_documents.ts new file mode 100644 index 0000000000000..b513fa79fc686 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/category_details_service/category_documents.ts @@ -0,0 +1,63 @@ +/* + * 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 { ISearchGeneric } from '@kbn/search-types'; +import { fromPromise } from 'xstate5'; +import { lastValueFrom } from 'rxjs'; +import { flattenHit } from '@kbn/data-service'; +import { LogCategoryDocument, LogCategoryDocumentsParams } from './types'; +import { createGetLogCategoryDocumentsRequestParams } from './queries'; + +export const getCategoryDocuments = ({ search }: { search: ISearchGeneric }) => + fromPromise< + { + categoryDocuments: LogCategoryDocument[]; + }, + LogCategoryDocumentsParams + >( + async ({ + input: { + index, + endTimestamp, + startTimestamp, + timeField, + messageField, + categoryTerms, + additionalFilters = [], + dataView, + }, + signal, + }) => { + const requestParams = createGetLogCategoryDocumentsRequestParams({ + index, + timeField, + messageField, + startTimestamp, + endTimestamp, + additionalFilters, + categoryTerms, + }); + + const { rawResponse } = await lastValueFrom( + search({ params: requestParams }, { abortSignal: signal }) + ); + + const categoryDocuments: LogCategoryDocument[] = + rawResponse.hits?.hits.map((hit) => { + return { + row: { + raw: hit._source, + flattened: flattenHit(hit, dataView), + }, + }; + }) ?? []; + + return { + categoryDocuments, + }; + } + ); diff --git a/x-pack/packages/observability/logs_overview/src/services/category_details_service/index.ts b/x-pack/packages/observability/logs_overview/src/services/category_details_service/index.ts new file mode 100644 index 0000000000000..5df79dbab2cbd --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/category_details_service/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './category_details_service'; diff --git a/x-pack/packages/observability/logs_overview/src/services/category_details_service/queries.ts b/x-pack/packages/observability/logs_overview/src/services/category_details_service/queries.ts new file mode 100644 index 0000000000000..cd1053077c334 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/category_details_service/queries.ts @@ -0,0 +1,58 @@ +/* + * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { createCategoryQuery } from '../categorize_logs_service/queries'; + +export const createGetLogCategoryDocumentsRequestParams = ({ + index, + timeField, + messageField, + startTimestamp, + endTimestamp, + additionalFilters = [], + categoryTerms = '', + documentCount = 20, +}: { + startTimestamp: string; + endTimestamp: string; + index: string; + timeField: string; + messageField: string; + additionalFilters?: QueryDslQueryContainer[]; + categoryTerms?: string; + documentCount?: number; +}) => { + return { + index, + size: documentCount, + track_total_hits: false, + sort: [{ [timeField]: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + exists: { + field: messageField, + }, + }, + { + range: { + [timeField]: { + gte: startTimestamp, + lte: endTimestamp, + format: 'strict_date_time', + }, + }, + }, + createCategoryQuery(messageField)(categoryTerms), + ...additionalFilters, + ], + }, + }, + }; +}; diff --git a/x-pack/packages/observability/logs_overview/src/services/category_details_service/types.ts b/x-pack/packages/observability/logs_overview/src/services/category_details_service/types.ts new file mode 100644 index 0000000000000..72369275578e3 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/category_details_service/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { ISearchGeneric } from '@kbn/search-types'; +import { type DataView } from '@kbn/data-views-plugin/common'; +import type { DataTableRecord } from '@kbn/discover-utils'; + +export interface LogCategoryDocument { + row: Pick; +} + +export interface LogCategoryDetailsParams { + additionalFilters: QueryDslQueryContainer[]; + endTimestamp: string; + index: string; + messageField: string; + startTimestamp: string; + timeField: string; + dataView: DataView; +} + +export interface CategoryDetailsServiceDependencies { + search: ISearchGeneric; +} + +export type LogCategoryDocumentsParams = LogCategoryDetailsParams & { categoryTerms: string }; diff --git a/x-pack/packages/observability/logs_overview/src/utils/log_category.ts b/x-pack/packages/observability/logs_overview/src/utils/log_category.ts new file mode 100644 index 0000000000000..3a5e72522d78e --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/utils/log_category.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogCategory } from '../types'; + +export const getLogCategoryTerms = (logCategory: LogCategory) => { + return logCategory.terms.split(' '); +}; diff --git a/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts b/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts index 0c8767c8702d4..15c318766be0e 100644 --- a/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts +++ b/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { type AbstractDataView } from '@kbn/data-views-plugin/common'; +import { type DataViewsContract, type DataView } from '@kbn/data-views-plugin/common'; import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; export type LogsSourceConfiguration = @@ -28,33 +28,68 @@ export interface IndexNameLogsSourceConfiguration { export interface DataViewLogsSourceConfiguration { type: 'data_view'; - dataView: AbstractDataView; + dataView: DataView; messageField?: string; } +export type ResolvedIndexNameLogsSourceConfiguration = IndexNameLogsSourceConfiguration & { + dataView: DataView; +}; + export const normalizeLogsSource = - ({ logsDataAccess }: { logsDataAccess: LogsDataAccessPluginStart }) => - async (logsSource: LogsSourceConfiguration): Promise => { + ({ + logsDataAccess, + dataViewsService, + }: { + logsDataAccess: LogsDataAccessPluginStart; + dataViewsService: DataViewsContract; + }) => + async ( + logsSource: LogsSourceConfiguration + ): Promise => { switch (logsSource.type) { case 'index_name': - return logsSource; + return { + ...logsSource, + dataView: await getDataViewForLogSource(logsSource, dataViewsService), + }; case 'shared_setting': const logSourcesFromSharedSettings = await logsDataAccess.services.logSourcesService.getLogSources(); - return { - type: 'index_name', + const sharedSettingLogsSource = { + type: 'index_name' as const, indexName: logSourcesFromSharedSettings .map((logSource) => logSource.indexPattern) .join(','), timestampField: logsSource.timestampField ?? '@timestamp', messageField: logsSource.messageField ?? 'message', }; - case 'data_view': return { - type: 'index_name', + ...sharedSettingLogsSource, + dataView: await getDataViewForLogSource(sharedSettingLogsSource, dataViewsService), + }; + case 'data_view': + const dataViewLogsSource = { + type: 'index_name' as const, indexName: logsSource.dataView.getIndexPattern(), timestampField: logsSource.dataView.timeFieldName ?? '@timestamp', messageField: logsSource.messageField ?? 'message', }; + return { + ...dataViewLogsSource, + dataView: logsSource.dataView, + }; } }; + +// Ad-hoc Data View +const getDataViewForLogSource = async ( + logSourceConfiguration: IndexNameLogsSourceConfiguration, + dataViewsService: DataViewsContract +) => { + const dataView = await dataViewsService.create({ + title: logSourceConfiguration.indexName, + timeFieldName: logSourceConfiguration.timestampField, + }); + return dataView; +}; diff --git a/x-pack/packages/observability/logs_overview/tsconfig.json b/x-pack/packages/observability/logs_overview/tsconfig.json index 886062ae8855f..29595ce0162fe 100644 --- a/x-pack/packages/observability/logs_overview/tsconfig.json +++ b/x-pack/packages/observability/logs_overview/tsconfig.json @@ -31,9 +31,15 @@ "@kbn/ml-random-sampler-utils", "@kbn/zod", "@kbn/calculate-auto", - "@kbn/discover-plugin", "@kbn/es-query", "@kbn/router-utils", "@kbn/share-plugin", + "@kbn/field-formats-plugin", + "@kbn/data-service", + "@kbn/discover-utils", + "@kbn/discover-plugin", + "@kbn/unified-data-table", + "@kbn/discover-contextual-components", + "@kbn/core-lifecycle-browser", ] } diff --git a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc index 10c8fe32cfe9c..f5e9f76c2ace6 100644 --- a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc +++ b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc @@ -11,6 +11,7 @@ "requiredPlugins": [ "charts", "data", + "fieldFormats", "dataViews", "discoverShared", "logsDataAccess", @@ -21,7 +22,7 @@ "optionalPlugins": [ "observabilityAIAssistant", ], - "requiredBundles": ["kibanaUtils", "kibanaReact"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "unifiedDocViewer"], "extraPublicDirs": ["common"] } } diff --git a/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts b/x-pack/plugins/observability_solution/logs_shared/public/plugin.tsx similarity index 97% rename from x-pack/plugins/observability_solution/logs_shared/public/plugin.ts rename to x-pack/plugins/observability_solution/logs_shared/public/plugin.tsx index fc17e9b17cc82..0321651607ed1 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts +++ b/x-pack/plugins/observability_solution/logs_shared/public/plugin.tsx @@ -61,6 +61,7 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass { logsDataAccess, observabilityAIAssistant, share, + fieldFormats, } = plugins; const logViews = this.logViews.start({ @@ -71,11 +72,14 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass { }); const LogsOverview = createLogsOverview({ + core, charts, logsDataAccess, search: data.search.search, uiSettings: settings, share, + dataViews, + fieldFormats, }); if (!observabilityAIAssistant) { diff --git a/x-pack/plugins/observability_solution/logs_shared/public/types.ts b/x-pack/plugins/observability_solution/logs_shared/public/types.ts index 4237c28c621b8..e2435fa1f4915 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/types.ts +++ b/x-pack/plugins/observability_solution/logs_shared/public/types.ts @@ -14,6 +14,7 @@ import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/pub import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { LogsSharedLocators } from '../common/locators'; import type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant'; import type { SelfContainedLogsOverview } from './components/logs_overview'; @@ -44,6 +45,7 @@ export interface LogsSharedClientStartDeps { observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; share: SharePluginStart; uiActions: UiActionsStart; + fieldFormats: FieldFormatsStart; } export type LogsSharedClientCoreSetup = CoreSetup< diff --git a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json index 788f55c9b6fc5..f171c79afccd0 100644 --- a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json +++ b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json @@ -48,5 +48,6 @@ "@kbn/observability-logs-overview", "@kbn/charts-plugin", "@kbn/core-ui-settings-common", + "@kbn/field-formats-plugin", ] } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f4a38ce6ee929..4c2bb57ded486 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2480,45 +2480,10 @@ "discover.localMenu.saveTitle": "Enregistrer", "discover.localMenu.shareSearchDescription": "Partager la recherche", "discover.localMenu.shareTitle": "Partager", - "discover.logs.dataTable.controlColumn.actions.button.degradedDoc": "Accès à un document dégradé avec le champ {ignoredProperty}", - "discover.logs.dataTable.controlColumn.actions.button.degradedDocNotPresent": "Tous les champs de ce document ont été analysés correctement", - "discover.logs.dataTable.controlColumn.actions.button.degradedDocPresent": "Ce document n'a pas pu être analysé correctement. Tous les champs n'ont pas été remplis correctement", - "discover.logs.dataTable.controlColumn.actions.button.stacktrace.available": "Traces d'appel disponibles", - "discover.logs.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "Traces d'appel indisponibles", - "discover.logs.dataTable.header.actions.tooltip.expand": "Développer les détails du log", - "discover.logs.dataTable.header.actions.tooltip.paragraph": "Les champs fournissant des informations exploitables, comme :", - "discover.logs.dataTable.header.actions.tooltip.stacktrace": "L'accès aux traces d'appel disponibles est basé sur :", - "discover.logs.dataTable.header.content.tooltip.paragraph1": "Affiche le {logLevel} du document et les champs {message}.", - "discover.logs.dataTable.header.content.tooltip.paragraph2": "Lorsque le champ de message est vide, l'une des informations suivantes s'affiche :", - "discover.logs.dataTable.header.popover.actions": "Actions", - "discover.logs.dataTable.header.popover.actions.lowercase": "actions", "discover.logs.dataTable.header.popover.content": "Contenu", "discover.logs.dataTable.header.popover.resource": "Ressource", - "discover.logs.dataTable.header.resource.tooltip.paragraph": "Les champs fournissant des informations sur la source du document, comme :", - "discover.logs.flyoutDetail.accordion.title.cloud": "Cloud", - "discover.logs.flyoutDetail.accordion.title.other": "Autre", - "discover.logs.flyoutDetail.accordion.title.serviceInfra": "Service et Infrastructure", - "discover.logs.flyoutDetail.label.cloudAvailabilityZone": "Zone de disponibilité du cloud", - "discover.logs.flyoutDetail.label.cloudInstanceId": "ID d'instance du cloud", - "discover.logs.flyoutDetail.label.cloudProjectId": "ID de projet du cloud", - "discover.logs.flyoutDetail.label.cloudProvider": "Fournisseur cloud", - "discover.logs.flyoutDetail.label.cloudRegion": "Région du cloud", - "discover.logs.flyoutDetail.label.dataset": "Ensemble de données", - "discover.logs.flyoutDetail.label.hostName": "Nom d'hôte", - "discover.logs.flyoutDetail.label.logPathFile": "Fichier de chemin d'accès au log", - "discover.logs.flyoutDetail.label.message": "Répartition du contenu", - "discover.logs.flyoutDetail.label.namespace": "Espace de nom", - "discover.logs.flyoutDetail.label.orchestratorClusterName": "Nom de cluster de l'orchestrateur", - "discover.logs.flyoutDetail.label.orchestratorResourceId": "ID de ressource de l'orchestrateur", - "discover.logs.flyoutDetail.label.service": "Service", - "discover.logs.flyoutDetail.label.shipper": "Agent de transfert", - "discover.logs.flyoutDetail.label.trace": "Trace", - "discover.logs.flyoutDetail.section.showMore": "+ {hiddenCount} autres", - "discover.logs.flyoutDetail.value.hover.copyToClipboard": "Copier dans le presse-papiers", "discover.logs.flyoutDetail.value.hover.filterFor": "Filtrer sur cette {value}", - "discover.logs.flyoutDetail.value.hover.filterForFieldPresent": "Filtrer sur le champ", "discover.logs.flyoutDetail.value.hover.filterOut": "Exclure cette {value}", - "discover.logs.flyoutDetail.value.hover.toggleColumn": "Afficher/Masquer la colonne dans le tableau", "discover.logs.popoverAction.closePopover": "Fermer la fenêtre contextuelle", "discover.logs.popoverAction.copyValue": "Copier la valeur", "discover.logs.popoverAction.copyValueAriaText": "Copier la valeur de {fieldName}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 27b79ebde86f6..e26596932ed93 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2479,45 +2479,10 @@ "discover.localMenu.saveTitle": "保存", "discover.localMenu.shareSearchDescription": "検索を共有します", "discover.localMenu.shareTitle": "共有", - "discover.logs.dataTable.controlColumn.actions.button.degradedDoc": "{ignoredProperty}フィールドの劣化したドキュメントにアクセス", - "discover.logs.dataTable.controlColumn.actions.button.degradedDocNotPresent": "このドキュメントのすべてのフィールドは正しく解析されました", - "discover.logs.dataTable.controlColumn.actions.button.degradedDocPresent": "このドキュメントを正しく解析できませんでした。一部のフィールドが正しく入力されていません", - "discover.logs.dataTable.controlColumn.actions.button.stacktrace.available": "スタックトレースがあります", - "discover.logs.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "スタックトレースがありません", - "discover.logs.dataTable.header.actions.tooltip.expand": "ログの詳細を展開", - "discover.logs.dataTable.header.actions.tooltip.paragraph": "次のようなアクショナブルな情報を提供するフィールド:", - "discover.logs.dataTable.header.actions.tooltip.stacktrace": "次に基づいて使用可能なスタックトレースにアクセス:", - "discover.logs.dataTable.header.content.tooltip.paragraph1": "ドキュメントの{logLevel}と{message}フィールドを表示します。", - "discover.logs.dataTable.header.content.tooltip.paragraph2": "メッセージフィールドが空のときには、次のいずれかが表示されます。", - "discover.logs.dataTable.header.popover.actions": "アクション", - "discover.logs.dataTable.header.popover.actions.lowercase": "アクション", "discover.logs.dataTable.header.popover.content": "コンテンツ", "discover.logs.dataTable.header.popover.resource": "リソース", - "discover.logs.dataTable.header.resource.tooltip.paragraph": "次のようなドキュメントのソースに関する情報を提供するフィールド:", - "discover.logs.flyoutDetail.accordion.title.cloud": "クラウド", - "discover.logs.flyoutDetail.accordion.title.other": "Other", - "discover.logs.flyoutDetail.accordion.title.serviceInfra": "サービスとインフラストラクチャー", - "discover.logs.flyoutDetail.label.cloudAvailabilityZone": "クラウドアベイラビリティゾーン", - "discover.logs.flyoutDetail.label.cloudInstanceId": "クラウドインスタンスID", - "discover.logs.flyoutDetail.label.cloudProjectId": "クラウドプロジェクトID", - "discover.logs.flyoutDetail.label.cloudProvider": "クラウドプロバイダー", - "discover.logs.flyoutDetail.label.cloudRegion": "クラウドリージョン", - "discover.logs.flyoutDetail.label.dataset": "データセット", - "discover.logs.flyoutDetail.label.hostName": "ホスト名", - "discover.logs.flyoutDetail.label.logPathFile": "ログパスファイル", - "discover.logs.flyoutDetail.label.message": "コンテンツの内訳", - "discover.logs.flyoutDetail.label.namespace": "名前空間", - "discover.logs.flyoutDetail.label.orchestratorClusterName": "オーケストレータークラスター名", - "discover.logs.flyoutDetail.label.orchestratorResourceId": "オーケストレーターリソースID", - "discover.logs.flyoutDetail.label.service": "サービス", - "discover.logs.flyoutDetail.label.shipper": "シッパー", - "discover.logs.flyoutDetail.label.trace": "トレース", - "discover.logs.flyoutDetail.section.showMore": "+ その他{hiddenCount}件", - "discover.logs.flyoutDetail.value.hover.copyToClipboard": "クリップボードにコピー", "discover.logs.flyoutDetail.value.hover.filterFor": "この{value}でフィルターを適用", - "discover.logs.flyoutDetail.value.hover.filterForFieldPresent": "フィールド表示のフィルター", "discover.logs.flyoutDetail.value.hover.filterOut": "この{value}を除外", - "discover.logs.flyoutDetail.value.hover.toggleColumn": "表の列を切り替える", "discover.logs.popoverAction.closePopover": "ポップオーバーを閉じる", "discover.logs.popoverAction.copyValue": "値をコピー", "discover.logs.popoverAction.copyValueAriaText": "{fieldName}の値をコピー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ee8601400fef0..bfff698aca31e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2481,45 +2481,10 @@ "discover.localMenu.saveTitle": "保存", "discover.localMenu.shareSearchDescription": "共享搜索", "discover.localMenu.shareTitle": "共享", - "discover.logs.dataTable.controlColumn.actions.button.degradedDoc": "包含 {ignoredProperty} 字段的已降级文档的访问权限", - "discover.logs.dataTable.controlColumn.actions.button.degradedDocNotPresent": "此文档中的所有字段均进行了正确解析", - "discover.logs.dataTable.controlColumn.actions.button.degradedDocPresent": "无法正确解析此文档。并非所有字段都进行了正确填充", - "discover.logs.dataTable.controlColumn.actions.button.stacktrace.available": "堆栈跟踪可用", - "discover.logs.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "堆栈跟踪不可用", - "discover.logs.dataTable.header.actions.tooltip.expand": "展开日志详情", - "discover.logs.dataTable.header.actions.tooltip.paragraph": "提供可操作信息的字段,例如:", - "discover.logs.dataTable.header.actions.tooltip.stacktrace": "基于以下项访问可用堆栈跟踪:", - "discover.logs.dataTable.header.content.tooltip.paragraph1": "显示该文档的 {logLevel} 和 {message} 字段。", - "discover.logs.dataTable.header.content.tooltip.paragraph2": "消息字段为空时,将显示以下项之一:", - "discover.logs.dataTable.header.popover.actions": "操作", - "discover.logs.dataTable.header.popover.actions.lowercase": "操作", "discover.logs.dataTable.header.popover.content": "内容", "discover.logs.dataTable.header.popover.resource": "资源", - "discover.logs.dataTable.header.resource.tooltip.paragraph": "提供有关文档来源信息的字段,例如:", - "discover.logs.flyoutDetail.accordion.title.cloud": "云", - "discover.logs.flyoutDetail.accordion.title.other": "其他", - "discover.logs.flyoutDetail.accordion.title.serviceInfra": "服务和基础设施", - "discover.logs.flyoutDetail.label.cloudAvailabilityZone": "云可用区", - "discover.logs.flyoutDetail.label.cloudInstanceId": "云实例 ID", - "discover.logs.flyoutDetail.label.cloudProjectId": "云项目 ID", - "discover.logs.flyoutDetail.label.cloudProvider": "云服务提供商", - "discover.logs.flyoutDetail.label.cloudRegion": "云区域", - "discover.logs.flyoutDetail.label.dataset": "数据集", - "discover.logs.flyoutDetail.label.hostName": "主机名", - "discover.logs.flyoutDetail.label.logPathFile": "日志路径文件", - "discover.logs.flyoutDetail.label.message": "内容细目", - "discover.logs.flyoutDetail.label.namespace": "命名空间", - "discover.logs.flyoutDetail.label.orchestratorClusterName": "Orchestrator 集群名称", - "discover.logs.flyoutDetail.label.orchestratorResourceId": "Orchestrator 资源 ID", - "discover.logs.flyoutDetail.label.service": "服务", - "discover.logs.flyoutDetail.label.shipper": "采集器", - "discover.logs.flyoutDetail.label.trace": "跟踪", - "discover.logs.flyoutDetail.section.showMore": "+ 另外 {hiddenCount} 个", - "discover.logs.flyoutDetail.value.hover.copyToClipboard": "复制到剪贴板", "discover.logs.flyoutDetail.value.hover.filterFor": "筛留此 {value}", - "discover.logs.flyoutDetail.value.hover.filterForFieldPresent": "筛留存在的字段", "discover.logs.flyoutDetail.value.hover.filterOut": "筛除此 {value}", - "discover.logs.flyoutDetail.value.hover.toggleColumn": "在表中切换列", "discover.logs.popoverAction.closePopover": "关闭弹出框", "discover.logs.popoverAction.copyValue": "复制值", "discover.logs.popoverAction.copyValueAriaText": "复制 {fieldName} 的值", diff --git a/yarn.lock b/yarn.lock index 0b67244d4058a..68e3633d66d28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4788,6 +4788,10 @@ version "0.0.0" uid "" +"@kbn/discover-contextual-components@link:packages/kbn-discover-contextual-components": + version "0.0.0" + uid "" + "@kbn/discover-customization-examples-plugin@link:examples/discover_customization_examples": version "0.0.0" uid ""