diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0f584440699689..e85da3abf6ec28 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1138,6 +1138,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql /x-pack/test/functional/apps/dataset_quality @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/test_suites/observability/dataset_quality @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/test_suites/observability/ @elastic/obs-ux-logs-team +/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview @elastic/obs-ux-logs-team # Observability onboarding tour /x-pack/plugins/observability_solution/observability_shared/public/components/tour @elastic/platform-onboarding diff --git a/packages/kbn-discover-utils/index.ts b/packages/kbn-discover-utils/index.ts index a409962230ce63..0434a05a9ce922 100644 --- a/packages/kbn-discover-utils/index.ts +++ b/packages/kbn-discover-utils/index.ts @@ -31,12 +31,17 @@ export { IgnoredReason, buildDataTableRecord, buildDataTableRecordList, + fieldConstants, formatFieldValue, formatHit, getDocId, + getLogDocumentOverview, getIgnoredReason, + getMessageFieldWithFallbacks, getShouldShowFieldHandler, isNestedFieldParent, isLegacyTableEnabled, usePager, } from './src'; + +export * from './src/types'; diff --git a/packages/kbn-discover-utils/src/field_constants.ts b/packages/kbn-discover-utils/src/field_constants.ts new file mode 100644 index 00000000000000..5cc8be86f06d31 --- /dev/null +++ b/packages/kbn-discover-utils/src/field_constants.ts @@ -0,0 +1,42 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +// Fields constants +export const TIMESTAMP_FIELD = '@timestamp'; +export const HOST_NAME_FIELD = 'host.name'; +export const LOG_LEVEL_FIELD = 'log.level'; +export const MESSAGE_FIELD = 'message'; +export const ERROR_MESSAGE_FIELD = 'error.message'; +export const EVENT_ORIGINAL_FIELD = 'event.original'; +export const TRACE_ID_FIELD = 'trace.id'; + +export const LOG_FILE_PATH_FIELD = 'log.file.path'; +export const DATASTREAM_NAMESPACE_FIELD = 'data_stream.namespace'; +export const DATASTREAM_DATASET_FIELD = 'data_stream.dataset'; + +// Resource Fields +export const AGENT_NAME_FIELD = 'agent.name'; +export const CLOUD_PROVIDER_FIELD = 'cloud.provider'; +export const CLOUD_REGION_FIELD = 'cloud.region'; +export const CLOUD_AVAILABILITY_ZONE_FIELD = 'cloud.availability_zone'; +export const CLOUD_PROJECT_ID_FIELD = 'cloud.project.id'; +export const CLOUD_INSTANCE_ID_FIELD = 'cloud.instance.id'; +export const SERVICE_NAME_FIELD = 'service.name'; +export const ORCHESTRATOR_CLUSTER_NAME_FIELD = 'orchestrator.cluster.name'; +export const ORCHESTRATOR_RESOURCE_ID_FIELD = 'orchestrator.resource.id'; +export const ORCHESTRATOR_NAMESPACE_FIELD = 'orchestrator.namespace'; +export const CONTAINER_NAME_FIELD = 'container.name'; +export const CONTAINER_ID_FIELD = 'container.id'; + +// Degraded Docs +export const DEGRADED_DOCS_FIELD = 'ignored_field_values'; + +// Error Stacktrace +export const ERROR_STACK_TRACE = 'error.stack_trace'; +export const ERROR_EXCEPTION_STACKTRACE = 'error.exception.stacktrace'; +export const ERROR_LOG_STACKTRACE = 'error.log.stacktrace'; diff --git a/packages/kbn-discover-utils/src/index.ts b/packages/kbn-discover-utils/src/index.ts index abc14f31911fe5..b1dc1901117462 100644 --- a/packages/kbn-discover-utils/src/index.ts +++ b/packages/kbn-discover-utils/src/index.ts @@ -7,5 +7,6 @@ */ export * from './constants'; +export * as fieldConstants from './field_constants'; export * from './hooks'; export * from './utils'; diff --git a/packages/kbn-discover-utils/src/types.ts b/packages/kbn-discover-utils/src/types.ts index 1fcc7c6a07c7f3..05d9a79b0bbbd4 100644 --- a/packages/kbn-discover-utils/src/types.ts +++ b/packages/kbn-discover-utils/src/types.ts @@ -46,3 +46,44 @@ type FormattedHitPair = readonly [ * Pairs array for each field in the hit */ export type FormattedHit = FormattedHitPair[]; + +export interface LogDocumentOverview + extends LogResourceFields, + LogStackTraceFields, + LogCloudFields { + '@timestamp': string; + 'log.level'?: string; + message?: string; + 'error.message'?: string; + 'event.original'?: string; + 'trace.id'?: string; + 'log.file.path'?: string; + 'data_stream.namespace': string; + 'data_stream.dataset': string; +} + +export interface LogResourceFields { + 'host.name'?: string; + 'service.name'?: string; + 'agent.name'?: string; + 'orchestrator.cluster.name'?: string; + 'orchestrator.cluster.id'?: string; + 'orchestrator.resource.id'?: string; + 'orchestrator.namespace'?: string; + 'container.name'?: string; + 'container.id'?: string; +} + +export interface LogStackTraceFields { + 'error.stack_trace'?: string; + 'error.exception.stacktrace'?: string; + 'error.log.stacktrace'?: string; +} + +export interface LogCloudFields { + 'cloud.provider'?: string; + 'cloud.region'?: string; + 'cloud.availability_zone'?: string; + 'cloud.project.id'?: string; + 'cloud.instance.id'?: string; +} diff --git a/packages/kbn-discover-utils/src/utils/get_log_document_overview.ts b/packages/kbn-discover-utils/src/utils/get_log_document_overview.ts new file mode 100644 index 00000000000000..92fb5e6c0834d8 --- /dev/null +++ b/packages/kbn-discover-utils/src/utils/get_log_document_overview.ts @@ -0,0 +1,92 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { DataTableRecord, LogDocumentOverview, fieldConstants, formatFieldValue } from '../..'; + +export function getLogDocumentOverview( + doc: DataTableRecord, + { dataView, fieldFormats }: { dataView: DataView; fieldFormats: FieldFormatsStart } +): LogDocumentOverview { + const formatField = (field: T) => { + return ( + field in doc.flattened + ? formatFieldValue( + doc.flattened[field], + doc.raw, + fieldFormats, + dataView, + dataView.fields.getByName(field) + ) + : undefined + ) as LogDocumentOverview[T]; + }; + + const levelArray = doc.flattened[fieldConstants.LOG_LEVEL_FIELD]; + const level = + Array.isArray(levelArray) && levelArray.length ? levelArray[0].toLowerCase() : levelArray; + const messageArray = doc.flattened[fieldConstants.MESSAGE_FIELD]; + const message = + Array.isArray(messageArray) && messageArray.length ? messageArray[0] : messageArray; + const errorMessageArray = doc.flattened[fieldConstants.ERROR_MESSAGE_FIELD]; + const errorMessage = + Array.isArray(errorMessageArray) && errorMessageArray.length + ? errorMessageArray[0] + : errorMessageArray; + const eventOriginalArray = doc.flattened[fieldConstants.EVENT_ORIGINAL_FIELD]; + const eventOriginal = + Array.isArray(eventOriginalArray) && eventOriginalArray.length + ? eventOriginalArray[0] + : eventOriginalArray; + const timestamp = formatField(fieldConstants.TIMESTAMP_FIELD); + + // Service + const serviceName = formatField(fieldConstants.SERVICE_NAME_FIELD); + const traceId = formatField(fieldConstants.TRACE_ID_FIELD); + + // Infrastructure + const hostname = formatField(fieldConstants.HOST_NAME_FIELD); + const orchestratorClusterName = formatField(fieldConstants.ORCHESTRATOR_CLUSTER_NAME_FIELD); + const orchestratorResourceId = formatField(fieldConstants.ORCHESTRATOR_RESOURCE_ID_FIELD); + + // Cloud + const cloudProvider = formatField(fieldConstants.CLOUD_PROVIDER_FIELD); + const cloudRegion = formatField(fieldConstants.CLOUD_REGION_FIELD); + const cloudAz = formatField(fieldConstants.CLOUD_AVAILABILITY_ZONE_FIELD); + const cloudProjectId = formatField(fieldConstants.CLOUD_PROJECT_ID_FIELD); + const cloudInstanceId = formatField(fieldConstants.CLOUD_INSTANCE_ID_FIELD); + + // Other + const logFilePath = formatField(fieldConstants.LOG_FILE_PATH_FIELD); + const namespace = formatField(fieldConstants.DATASTREAM_NAMESPACE_FIELD); + const dataset = formatField(fieldConstants.DATASTREAM_DATASET_FIELD); + const agentName = formatField(fieldConstants.AGENT_NAME_FIELD); + + return { + [fieldConstants.LOG_LEVEL_FIELD]: level, + [fieldConstants.TIMESTAMP_FIELD]: timestamp, + [fieldConstants.MESSAGE_FIELD]: message, + [fieldConstants.ERROR_MESSAGE_FIELD]: errorMessage, + [fieldConstants.EVENT_ORIGINAL_FIELD]: eventOriginal, + [fieldConstants.SERVICE_NAME_FIELD]: serviceName, + [fieldConstants.TRACE_ID_FIELD]: traceId, + [fieldConstants.HOST_NAME_FIELD]: hostname, + [fieldConstants.ORCHESTRATOR_CLUSTER_NAME_FIELD]: orchestratorClusterName, + [fieldConstants.ORCHESTRATOR_RESOURCE_ID_FIELD]: orchestratorResourceId, + [fieldConstants.CLOUD_PROVIDER_FIELD]: cloudProvider, + [fieldConstants.CLOUD_REGION_FIELD]: cloudRegion, + [fieldConstants.CLOUD_AVAILABILITY_ZONE_FIELD]: cloudAz, + [fieldConstants.CLOUD_PROJECT_ID_FIELD]: cloudProjectId, + [fieldConstants.CLOUD_INSTANCE_ID_FIELD]: cloudInstanceId, + [fieldConstants.LOG_FILE_PATH_FIELD]: logFilePath, + [fieldConstants.DATASTREAM_NAMESPACE_FIELD]: namespace, + [fieldConstants.DATASTREAM_DATASET_FIELD]: dataset, + [fieldConstants.AGENT_NAME_FIELD]: agentName, + }; +} diff --git a/packages/kbn-discover-utils/src/utils/get_message_field_with_fallbacks.ts b/packages/kbn-discover-utils/src/utils/get_message_field_with_fallbacks.ts new file mode 100644 index 00000000000000..09fda703faee42 --- /dev/null +++ b/packages/kbn-discover-utils/src/utils/get_message_field_with_fallbacks.ts @@ -0,0 +1,27 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { fieldConstants } from '..'; +import { LogDocumentOverview } from '../types'; + +export const getMessageFieldWithFallbacks = (doc: LogDocumentOverview) => { + const rankingOrder = [ + fieldConstants.MESSAGE_FIELD, + fieldConstants.ERROR_MESSAGE_FIELD, + fieldConstants.EVENT_ORIGINAL_FIELD, + ] as const; + + for (const rank of rankingOrder) { + if (doc[rank] !== undefined && doc[rank] !== null) { + return { field: rank, value: doc[rank] }; + } + } + + // If none of the ranks (fallbacks) are present + return { field: undefined }; +}; diff --git a/packages/kbn-discover-utils/src/utils/index.ts b/packages/kbn-discover-utils/src/utils/index.ts index 8415fc7df07103..399d4e8ba298e7 100644 --- a/packages/kbn-discover-utils/src/utils/index.ts +++ b/packages/kbn-discover-utils/src/utils/index.ts @@ -11,6 +11,8 @@ export * from './format_hit'; export * from './format_value'; export * from './get_doc_id'; export * from './get_ignored_reason'; +export * from './get_log_document_overview'; +export * from './get_message_field_with_fallbacks'; export * from './get_should_show_field_handler'; export * from './nested_fields'; export { isLegacyTableEnabled } from './is_legacy_table_enabled'; diff --git a/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer.tsx b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer.tsx index fab4673eb8ed5b..74fbfcf0a61330 100644 --- a/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer.tsx +++ b/packages/kbn-unified-doc-viewer/src/components/doc_viewer/doc_viewer.tsx @@ -22,22 +22,24 @@ export interface DocViewerProps extends DocViewRenderProps { * a `render` function. */ export function DocViewer({ docViews, ...renderProps }: DocViewerProps) { - const tabs = docViews.map(({ id, title, render, component }: DocView) => { - return { - id: `kbn_doc_viewer_tab_${id}`, - name: title, - content: ( - - ), - ['data-test-subj']: `docViewerTab-${id}`, - }; - }); + const tabs = docViews + .filter(({ enabled }) => enabled) // Filter out disabled doc views + .map(({ id, title, render, component }: DocView) => { + return { + id: `kbn_doc_viewer_tab_${id}`, + name: title, + content: ( + + ), + ['data-test-subj']: `docViewerTab-${id}`, + }; + }); if (!tabs.length) { // There's a minimum of 2 tabs active in Discover. diff --git a/packages/kbn-unified-doc-viewer/src/services/doc_views_registry.test.tsx b/packages/kbn-unified-doc-viewer/src/services/doc_views_registry.test.tsx index e8f631814d2875..9154866120b017 100644 --- a/packages/kbn-unified-doc-viewer/src/services/doc_views_registry.test.tsx +++ b/packages/kbn-unified-doc-viewer/src/services/doc_views_registry.test.tsx @@ -74,6 +74,25 @@ describe('DocViewerRegistry', () => { }); }); + describe('#enableById & #disableById', () => { + test('should enable/disable a doc view given the passed id', () => { + const registry = new DocViewsRegistry([fnDocView, componentDocView]); + + const docViews = registry.getAll(); + + expect(docViews[0]).toHaveProperty('enabled', true); + expect(docViews[1]).toHaveProperty('enabled', true); + + registry.disableById('function-doc-view'); + + expect(registry.getAll()[0]).toHaveProperty('enabled', false); + + registry.enableById('function-doc-view'); + + expect(registry.getAll()[0]).toHaveProperty('enabled', true); + }); + }); + describe('#clone', () => { test('should return a new DocViewRegistry instance starting from the current one', () => { const registry = new DocViewsRegistry([fnDocView, componentDocView]); @@ -84,6 +103,24 @@ describe('DocViewerRegistry', () => { expect(docViews[0]).toHaveProperty('id', 'function-doc-view'); expect(docViews[1]).toHaveProperty('id', 'component-doc-view'); expect(registry).not.toBe(clonedRegistry); + + // Test against shared references between clones + expect(clonedRegistry).not.toBe(registry); + + // Mutating a cloned registry should not affect the original registry + registry.disableById('function-doc-view'); + expect(registry.getAll()[0]).toHaveProperty('enabled', false); + expect(clonedRegistry.getAll()[0]).toHaveProperty('enabled', true); + + clonedRegistry.add({ + id: 'additional-doc-view', + order: 20, + title: 'Render function', + render: jest.fn(), + }); + + expect(registry.getAll().length).toBe(2); + expect(clonedRegistry.getAll().length).toBe(3); }); }); }); diff --git a/packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts b/packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts index 9cada7ed497a39..8babc9c5aa9451 100644 --- a/packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts +++ b/packages/kbn-unified-doc-viewer/src/services/doc_views_registry.ts @@ -15,6 +15,10 @@ export enum ElasticRequestState { NotFoundDataView, } +const defaultDocViewConfig = { + enabled: true, +}; + export class DocViewsRegistry { private docViews: Map; @@ -22,7 +26,9 @@ export class DocViewsRegistry { if (initialValue instanceof DocViewsRegistry) { this.docViews = new Map(initialValue.docViews); } else if (Array.isArray(initialValue)) { - this.docViews = new Map(initialValue.map((docView) => [docView.id, docView])); + this.docViews = new Map( + initialValue.map((docView) => [docView.id, this.createDocView(docView)]) + ); } else { this.docViews = new Map(); } @@ -41,7 +47,7 @@ export class DocViewsRegistry { ); } - this.docViews.set(docView.id, docView); + this.docViews.set(docView.id, this.createDocView(docView)); // Sort the doc views at insertion time to perform this operation once and not on every retrieval. this.sortDocViews(); } @@ -50,8 +56,30 @@ export class DocViewsRegistry { this.docViews.delete(id); } + enableById(id: string) { + const docView = this.docViews.get(id); + if (docView) { + docView.enabled = true; + } else { + throw new Error( + `DocViewsRegistry#enableById: there is no DocView registered with id "${id}".` + ); + } + } + + disableById(id: string) { + const docView = this.docViews.get(id); + if (docView) { + docView.enabled = false; + } else { + throw new Error( + `DocViewsRegistry#disableById: there is no DocView registered with id "${id}".` + ); + } + } + clone() { - return new DocViewsRegistry(this); + return new DocViewsRegistry(this.getAll()); } private sortDocViews() { @@ -61,4 +89,8 @@ export class DocViewsRegistry { this.docViews = new Map(sortedEntries); } + + private createDocView(docView: DocView) { + return { ...defaultDocViewConfig, ...docView }; + } } diff --git a/packages/kbn-unified-doc-viewer/src/services/types.ts b/packages/kbn-unified-doc-viewer/src/services/types.ts index bcf64c817e8873..115583c738bb09 100644 --- a/packages/kbn-unified-doc-viewer/src/services/types.ts +++ b/packages/kbn-unified-doc-viewer/src/services/types.ts @@ -60,6 +60,7 @@ export interface BaseDocViewInput { id: string; order: number; title: string; + enabled?: boolean; } export interface RenderDocViewInput extends BaseDocViewInput { diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/index.ts b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/index.ts new file mode 100644 index 00000000000000..69f01c944ad253 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/index.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 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 or the Server + * Side Public License, v 1. + */ +import { LogsOverview } from './logs_overview'; + +// Required for usage in React.lazy +// eslint-disable-next-line import/no-default-export +export default LogsOverview; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.test.tsx new file mode 100644 index 00000000000000..7b9379654cec12 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.test.tsx @@ -0,0 +1,162 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { LogsOverview } from './logs_overview'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; +import { buildDataTableRecord } from '@kbn/discover-utils'; +import { setUnifiedDocViewerServices } from '../../plugin'; +import { mockUnifiedDocViewerServices } from '../../__mocks__'; + +const DATASET_NAME = 'logs.overview'; +const NAMESPACE = 'default'; +const DATA_STREAM_NAME = `logs-${DATASET_NAME}-${NAMESPACE}`; +const NOW = Date.now(); + +const dataView = { + fields: { + getAll: () => + [ + '_index', + 'message', + 'log.level', + 'service.name', + 'host.name', + 'trace.id', + 'orchestrator.cluster.id', + 'orchestrator.cluster.name', + 'orchestrator.resource.id', + 'cloud.provider', + 'cloud.region', + 'cloud.availability_zone', + 'cloud.project.id', + 'cloud.instance.id', + 'agent.name', + ].map((name) => ({ + name, + type: 'string', + scripted: false, + filterable: true, + })), + }, + metaFields: ['_index', '_score'], + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), +} as unknown as DataView; + +dataView.fields.getByName = (name: string) => { + return dataView.fields.getAll().find((field) => field.name === name); +}; + +const fullHit = buildDataTableRecord( + { + _index: DATA_STREAM_NAME, + _id: DATA_STREAM_NAME, + _score: 1, + _source: { + '@timestamp': NOW + 1000, + message: 'full document', + log: { level: 'info', file: { path: '/logs.overview.log' } }, + data_stream: { + type: 'logs', + dataset: DATASET_NAME, + namespace: NAMESPACE, + }, + 'service.name': DATASET_NAME, + 'host.name': 'gke-edge-oblt-pool', + 'trace.id': 'abcdef', + orchestrator: { + cluster: { + id: 'my-cluster-id', + name: 'my-cluster-name', + }, + resource: { + id: 'orchestratorResourceId', + }, + }, + cloud: { + provider: ['gcp'], + region: 'us-central-1', + availability_zone: 'us-central-1a', + project: { + id: 'elastic-project', + }, + instance: { + id: 'BgfderflkjTheUiGuy', + }, + }, + 'agent.name': 'node', + }, + }, + dataView +); + +setUnifiedDocViewerServices(mockUnifiedDocViewerServices); + +const renderLogsOverview = (props: Partial = {}) => { + const { rerender: baseRerender, ...tools } = render( + + ); + + const rerender = (rerenderProps: Partial) => + baseRerender(); + + return { rerender, ...tools }; +}; + +describe('LogsOverview', () => { + beforeEach(() => renderLogsOverview()); + + describe('Header section', () => { + it('should display a timestamp badge', async () => { + expect(screen.queryByTestId('unifiedDocViewLogsOverviewTimestamp')).toBeInTheDocument(); + }); + + it('should display a log level badge when available', async () => { + expect(screen.queryByTestId('unifiedDocViewLogsOverviewLogLevel')).toBeInTheDocument(); + }); + + it('should display a message code block when available', async () => { + expect(screen.queryByTestId('unifiedDocViewLogsOverviewMessage')).toBeInTheDocument(); + }); + }); + + describe('Highlights section', () => { + it('should load the service container with all fields', async () => { + expect( + screen.queryByTestId('unifiedDocViewLogsOverviewHighlightSectionServiceInfra') + ).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewService')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewTrace')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewHostName')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewClusterName')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewResourceId')).toBeInTheDocument(); + }); + + it('should load the cloud container with all fields', async () => { + expect( + screen.queryByTestId('unifiedDocViewLogsOverviewHighlightSectionCloud') + ).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewCloudProvider')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewCloudRegion')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewCloudAz')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewCloudProjectId')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewCloudInstanceId')).toBeInTheDocument(); + }); + + it('should load the other container with all fields', async () => { + expect( + screen.queryByTestId('unifiedDocViewLogsOverviewHighlightSectionOther') + ).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewLogPathFile')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewNamespace')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewDataset')).toBeInTheDocument(); + expect(screen.queryByTestId('unifiedDocViewLogsOverviewLogShipper')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.tsx new file mode 100644 index 00000000000000..564ce902f7238d --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.tsx @@ -0,0 +1,42 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; +import { getLogDocumentOverview } from '@kbn/discover-utils'; +import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import { LogsOverviewHeader } from './logs_overview_header'; +import { LogsOverviewHighlights } from './logs_overview_highlights'; +import { FieldActionsProvider } from '../../hooks/use_field_actions'; +import { getUnifiedDocViewerServices } from '../../plugin'; + +export function LogsOverview({ + columns, + dataView, + hit, + filter, + onAddColumn, + onRemoveColumn, +}: DocViewRenderProps) { + const { fieldFormats } = getUnifiedDocViewerServices(); + const parsedDoc = getLogDocumentOverview(hit, { dataView, fieldFormats }); + + return ( + + + + + + + ); +} diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview_header.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview_header.tsx new file mode 100644 index 00000000000000..4bbc2993dedb04 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview_header.tsx @@ -0,0 +1,101 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiAccordion, + useGeneratedHtmlId, + EuiTitle, +} from '@elastic/eui'; +import { + LogDocumentOverview, + fieldConstants, + getMessageFieldWithFallbacks, +} from '@kbn/discover-utils'; +import { i18n } from '@kbn/i18n'; +import { Timestamp } from './sub_components/timestamp'; +import { HoverActionPopover } from './sub_components/hover_popover_action'; +import { LogLevel } from './sub_components/log_level'; + +export const contentLabel = i18n.translate('unifiedDocViewer.docView.logsOverview.label.content', { + defaultMessage: 'Content breakdown', +}); + +export function LogsOverviewHeader({ doc }: { doc: LogDocumentOverview }) { + const hasLogLevel = Boolean(doc[fieldConstants.LOG_LEVEL_FIELD]); + const hasTimestamp = Boolean(doc[fieldConstants.TIMESTAMP_FIELD]); + const { field, value } = getMessageFieldWithFallbacks(doc); + const hasBadges = hasTimestamp || hasLogLevel; + const hasMessageField = field && value; + const hasFlyoutHeader = hasMessageField || hasBadges; + + const accordionId = useGeneratedHtmlId({ + prefix: contentLabel, + }); + + const accordionTitle = ( + +

{contentLabel}

+
+ ); + + const logLevelAndTimestamp = hasBadges && ( + + {doc[fieldConstants.LOG_LEVEL_FIELD] && ( + + + + )} + {hasTimestamp && } + + ); + + const contentField = hasMessageField && ( + + + + {field} + + {logLevelAndTimestamp} + + + + {value} + + + + ); + + return hasFlyoutHeader ? ( + + {hasMessageField ? contentField : logLevelAndTimestamp} + + ) : null; +} diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview_highlights.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview_highlights.tsx new file mode 100644 index 00000000000000..a7d998b0f1ebaa --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview_highlights.tsx @@ -0,0 +1,253 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { CloudProvider, CloudProviderIcon } from '@kbn/custom-icons'; +import { first } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { DataTableRecord, LogDocumentOverview, fieldConstants } from '@kbn/discover-utils'; +import { HighlightField } from './sub_components/highlight_field'; +import { HighlightSection } from './sub_components/highlight_section'; + +export function LogsOverviewHighlights({ + formattedDoc, + flattenedDoc, +}: { + formattedDoc: LogDocumentOverview; + flattenedDoc: DataTableRecord['flattened']; +}) { + const getHighlightProps = (field: keyof LogDocumentOverview) => ({ + field, + formattedValue: formattedDoc[field], + value: flattenedDoc[field], + }); + + return ( + <> + {/* Service & Infrastructure highlight */} + + {formattedDoc[fieldConstants.SERVICE_NAME_FIELD] && ( + + )} + {formattedDoc[fieldConstants.HOST_NAME_FIELD] && ( + + )} + {formattedDoc[fieldConstants.TRACE_ID_FIELD] && ( + + )} + {formattedDoc[fieldConstants.ORCHESTRATOR_CLUSTER_NAME_FIELD] && ( + + )} + {formattedDoc[fieldConstants.ORCHESTRATOR_RESOURCE_ID_FIELD] && ( + + )} + + {/* Cloud highlight */} + + {formattedDoc[fieldConstants.CLOUD_PROVIDER_FIELD] && ( + + } + {...getHighlightProps(fieldConstants.CLOUD_PROVIDER_FIELD)} + /> + )} + {formattedDoc[fieldConstants.CLOUD_REGION_FIELD] && ( + + )} + {formattedDoc[fieldConstants.CLOUD_AVAILABILITY_ZONE_FIELD] && ( + + )} + {formattedDoc[fieldConstants.CLOUD_PROJECT_ID_FIELD] && ( + + )} + {formattedDoc[fieldConstants.CLOUD_INSTANCE_ID_FIELD] && ( + + )} + + {/* Other highlights */} + + {formattedDoc[fieldConstants.LOG_FILE_PATH_FIELD] && ( + + )} + {formattedDoc[fieldConstants.DATASTREAM_DATASET_FIELD] && ( + + )} + {formattedDoc[fieldConstants.DATASTREAM_NAMESPACE_FIELD] && ( + + )} + {formattedDoc[fieldConstants.AGENT_NAME_FIELD] && ( + + )} + + + ); +} + +const serviceLabel = i18n.translate('unifiedDocViewer.docView.logsOverview.label.service', { + defaultMessage: 'Service', +}); + +const traceLabel = i18n.translate('unifiedDocViewer.docView.logsOverview.label.trace', { + defaultMessage: 'Trace', +}); + +const hostNameLabel = i18n.translate('unifiedDocViewer.docView.logsOverview.label.hostName', { + defaultMessage: 'Host name', +}); + +const serviceInfraAccordionTitle = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.accordion.title.serviceInfra', + { + defaultMessage: 'Service & Infrastructure', + } +); + +const cloudAccordionTitle = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.accordion.title.cloud', + { + defaultMessage: 'Cloud', + } +); + +const otherAccordionTitle = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.accordion.title.other', + { + defaultMessage: 'Other', + } +); + +const orchestratorClusterNameLabel = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.label.orchestratorClusterName', + { + defaultMessage: 'Orchestrator cluster Name', + } +); + +const orchestratorResourceIdLabel = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.label.orchestratorResourceId', + { + defaultMessage: 'Orchestrator resource ID', + } +); + +const cloudProviderLabel = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.label.cloudProvider', + { + defaultMessage: 'Cloud provider', + } +); + +const cloudRegionLabel = i18n.translate('unifiedDocViewer.docView.logsOverview.label.cloudRegion', { + defaultMessage: 'Cloud region', +}); + +const cloudAvailabilityZoneLabel = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.label.cloudAvailabilityZone', + { + defaultMessage: 'Cloud availability zone', + } +); + +const cloudProjectIdLabel = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.label.cloudProjectId', + { + defaultMessage: 'Cloud project ID', + } +); + +const cloudInstanceIdLabel = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.label.cloudInstanceId', + { + defaultMessage: 'Cloud instance ID', + } +); + +const logPathFileLabel = i18n.translate('unifiedDocViewer.docView.logsOverview.label.logPathFile', { + defaultMessage: 'Log path file', +}); + +const namespaceLabel = i18n.translate('unifiedDocViewer.docView.logsOverview.label.namespace', { + defaultMessage: 'Namespace', +}); + +const datasetLabel = i18n.translate('unifiedDocViewer.docView.logsOverview.label.dataset', { + defaultMessage: 'Dataset', +}); + +const shipperLabel = i18n.translate('unifiedDocViewer.docView.logsOverview.label.shipper', { + defaultMessage: 'Shipper', +}); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/highlight_field.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/highlight_field.tsx new file mode 100644 index 00000000000000..176f8cf2b2a775 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/highlight_field.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 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 or the Server + * Side Public License, v 1. + */ + +import { EuiBadge, EuiFlexGroup, EuiText, EuiTitle } from '@elastic/eui'; +import { css } from '@emotion/react'; +import React, { ReactNode } from 'react'; +import { dynamic } from '@kbn/shared-ux-utility'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { HoverActionPopover } from './hover_popover_action'; + +const HighlightFieldDescription = dynamic(() => import('./highlight_field_description')); + +interface HighlightFieldProps { + field: string; + formattedValue?: string; + icon?: ReactNode; + label: string; + useBadge?: boolean; + value?: unknown; +} + +export function HighlightField({ + field, + formattedValue, + icon, + label, + useBadge = false, + value, + ...props +}: HighlightFieldProps) { + return formattedValue && value ? ( +
+ + + {label} + + + + + + {icon} + {useBadge ? ( + + {formattedValue} + + ) : ( + + )} + + +
+ ) : null; +} + +const fieldNameStyle = css` + color: ${euiThemeVars.euiColorDarkShade}; +`; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_field_description.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/highlight_field_description.tsx similarity index 68% rename from x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_field_description.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/highlight_field_description.tsx index 0ecf46a527d86c..bfe2ef236da972 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_field_description.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/highlight_field_description.tsx @@ -1,11 +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. + * 2.0 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 or the Server + * Side Public License, v 1. */ -import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiIconTip } from '@elastic/eui'; import { EcsFlat } from '@elastic/ecs'; import { FieldIcon } from '@kbn/react-field'; import React from 'react'; @@ -17,12 +18,8 @@ export function HighlightFieldDescription({ fieldName }: { fieldName: string }) const title = ( - {type && ( - - - - )} - {fieldName} + {type && } + {fieldName} ); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/highlight_section.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/highlight_section.tsx new file mode 100644 index 00000000000000..f0e6b1cd5943f0 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/highlight_section.tsx @@ -0,0 +1,94 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { PropsWithChildren, useReducer } from 'react'; +import { + EuiAccordion, + EuiFlexGrid, + EuiHorizontalRule, + EuiTitle, + useGeneratedHtmlId, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; + +interface HighlightSectionProps { + title: string; + visibleItems?: number; +} + +export function HighlightSection({ + children, + title, + visibleItems = 6, + ...props +}: PropsWithChildren) { + const validChildren = React.Children.toArray(children).filter(Boolean); + const childrenLength = validChildren.length; + const shouldRenderSection = childrenLength > 0; + const limitedChildren = validChildren.slice(0, visibleItems - 1); + const [isListExpanded, expandList] = useReducer(() => true, childrenLength <= visibleItems); + + const accordionId = useGeneratedHtmlId({ + prefix: title, + }); + + const showMoreButtonLabel = i18n.translate( + 'unifiedDocViewer.docView.logsOverview.section.showMore', + { + defaultMessage: '+ {count} more', + values: { count: childrenLength - limitedChildren.length }, + } + ); + + const showMoreButton = ( + + {showMoreButtonLabel} + + ); + + limitedChildren.push(showMoreButton); + + const accordionTitle = ( + +

{title}

+
+ ); + + const displayedItems = isListExpanded ? validChildren : limitedChildren; + + return shouldRenderSection ? ( + <> + + + {displayedItems} + + + + + ) : null; +} + +// Applying this custom css rule remove the need for custom runtime js to compute a responsive column layout +const gridStyle = css` + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); +`; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/hover_popover_action.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/hover_popover_action.tsx similarity index 73% rename from x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/hover_popover_action.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/hover_popover_action.tsx index 1f9855c28a86b6..7df7aa7950e85d 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/hover_popover_action.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/hover_popover_action.tsx @@ -1,8 +1,9 @@ /* * 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. + * 2.0 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 or the Server + * Side Public License, v 1. */ import React, { useRef, useState } from 'react'; @@ -15,13 +16,13 @@ import { PopoverAnchorPosition, type EuiPopoverProps, } from '@elastic/eui'; -import { useHoverActions } from '../../../hooks/use_hover_actions'; +import { useUIFieldActions } from '../../../hooks/use_field_actions'; interface HoverPopoverActionProps { children: React.ReactChild; field: string; - value: string; - title?: string; + value: unknown; + title?: unknown; anchorPosition?: PopoverAnchorPosition; display?: EuiPopoverProps['display']; } @@ -36,7 +37,7 @@ export const HoverActionPopover = ({ }: HoverPopoverActionProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const leaveTimer = useRef(null); - const hoverActions = useHoverActions({ field, value }); + const uiFieldActions = useUIFieldActions({ field, value }); // The timeout hack is required because we are using a Popover which ideally should be used with a mouseclick, // but we are using it as a Tooltip. Which means we now need to manually handle the open and close @@ -54,11 +55,12 @@ export const HoverActionPopover = ({ }; return ( -
+ )} - {hoverActions.map((action) => ( - + {uiFieldActions.map((action) => ( + action.onClick()} + aria-label={action.label} + onClick={action.onClick} /> ))} -
+ ); }; + +const closePopoverPlaceholder = () => {}; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/log_level.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/log_level.tsx new file mode 100644 index 00000000000000..5fcfaa871ddf2c --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/log_level.tsx @@ -0,0 +1,46 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiBadge, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { LogDocumentOverview } from '@kbn/discover-utils'; + +const LEVEL_DICT = { + error: 'danger', + warn: 'warning', + info: 'primary', + debug: 'accent', +} as const; + +type Level = keyof typeof LEVEL_DICT; + +interface LogLevelProps { + level: LogDocumentOverview['log.level']; +} + +export function LogLevel({ level }: LogLevelProps) { + const { euiTheme } = useEuiTheme(); + + if (!level) return null; + const colorName = LEVEL_DICT[level as Level]; + const computedColor = colorName ? euiTheme.colors[colorName] : null; + + return ( + + {level} + + ); +} diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/timestamp.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/timestamp.tsx similarity index 58% rename from x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/timestamp.tsx rename to src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/timestamp.tsx index da9e656a9f01d9..16028be05fb950 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/timestamp.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview/sub_components/timestamp.tsx @@ -1,23 +1,23 @@ /* * 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. + * 2.0 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 or the Server + * Side Public License, v 1. */ import React from 'react'; import { EuiBadge } from '@elastic/eui'; -import { FlyoutDoc } from '../../../../common/document'; interface TimestampProps { - timestamp: FlyoutDoc['@timestamp']; + timestamp?: string; } export function Timestamp({ timestamp }: TimestampProps) { if (!timestamp) return null; return ( - + {timestamp} ); diff --git a/src/plugins/unified_doc_viewer/public/hooks/use_field_actions.tsx b/src/plugins/unified_doc_viewer/public/hooks/use_field_actions.tsx new file mode 100644 index 00000000000000..e703386818f895 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/hooks/use_field_actions.tsx @@ -0,0 +1,127 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { useMemo } from 'react'; +import createContainer from 'constate'; +import { copyToClipboard, IconType } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; + +interface WithFieldParam { + field: string; +} + +interface WithValueParam { + value: unknown; +} + +interface TFieldActionParams extends WithFieldParam, WithValueParam {} + +export interface TFieldAction { + id: string; + iconType: IconType; + label: string; + onClick: () => void; +} + +type UseFieldActionsDeps = Pick< + DocViewRenderProps, + 'columns' | 'filter' | 'onAddColumn' | 'onRemoveColumn' +>; + +/** + * Higher level hook that wraps the logic for the requires actions on a field. + */ +const useFieldActions = ({ columns, filter, onAddColumn, onRemoveColumn }: UseFieldActionsDeps) => { + return useMemo( + () => ({ + addColumn: onAddColumn, + addFilterExist: ({ field }: WithFieldParam) => filter && filter('_exists_', field, '+'), + addFilterIn: ({ field, value }: TFieldActionParams) => filter && filter(field, value, '+'), + addFilterOut: ({ field, value }: TFieldActionParams) => filter && filter(field, value, '-'), + copyToClipboard, + removeColumn: onRemoveColumn, + toggleFieldColumn: ({ field }: WithFieldParam) => { + if (!columns) return; + const isFieldInTable = columns.includes(field); + if (isFieldInTable && onRemoveColumn) { + onRemoveColumn(field); + } else if (!isFieldInTable && onAddColumn) { + onAddColumn(field); + } + }, + }), + [columns, filter, onAddColumn, onRemoveColumn] + ); +}; + +export const [FieldActionsProvider, useFieldActionsContext] = createContainer(useFieldActions); + +/** + * This is a preset of the UI elements and related actions that can be used to build an action bar anywhere in a DocView + */ +export const useUIFieldActions = ({ field, value }: TFieldActionParams): TFieldAction[] => { + const actions = useFieldActionsContext(); + + return useMemo( + () => [ + { + id: 'addFilterInAction', + iconType: 'plusInCircle', + label: filterForValueLabel, + onClick: () => actions.addFilterIn({ field, value }), + }, + { + id: 'addFilterOutremoveFromFilterAction', + iconType: 'minusInCircle', + label: filterOutValueLabel, + onClick: () => actions.addFilterOut({ field, value }), + }, + { + id: 'addFilterExistAction', + iconType: 'filter', + label: filterForFieldPresentLabel, + onClick: () => actions.addFilterExist({ field }), + }, + { + id: 'toggleFieldColumnAction', + iconType: 'listAdd', + label: toggleColumnLabel, + onClick: () => actions.toggleFieldColumn({ field }), + }, + { + id: 'copyToClipboardAction', + iconType: 'copyClipboard', + label: copyToClipboardLabel, + onClick: () => actions.copyToClipboard(value as string), + }, + ], + [actions, field, value] + ); +}; + +const filterForValueLabel = i18n.translate('unifiedDocViewer.fieldActions.filterForValue', { + defaultMessage: 'Filter for value', +}); + +const filterOutValueLabel = i18n.translate('unifiedDocViewer.fieldActions.filterOutValue', { + defaultMessage: 'Filter out value', +}); + +const filterForFieldPresentLabel = i18n.translate( + 'unifiedDocViewer.fieldActions.filterForFieldPresent', + { defaultMessage: 'Filter for field present' } +); + +const toggleColumnLabel = i18n.translate('unifiedDocViewer.fieldActions.toggleColumn', { + defaultMessage: 'Toggle column in table', +}); + +const copyToClipboardLabel = i18n.translate('unifiedDocViewer.fieldActions.copyToClipboard', { + defaultMessage: 'Copy to clipboard', +}); diff --git a/src/plugins/unified_doc_viewer/public/plugin.tsx b/src/plugins/unified_doc_viewer/public/plugin.tsx index 6dc45274418228..19c0b29a916b3b 100644 --- a/src/plugins/unified_doc_viewer/public/plugin.tsx +++ b/src/plugins/unified_doc_viewer/public/plugin.tsx @@ -16,14 +16,26 @@ import { createGetterSetter, Storage } from '@kbn/kibana-utils-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { CoreStart } from '@kbn/core/public'; +import { dynamic } from '@kbn/shared-ux-utility'; import type { UnifiedDocViewerServices } from './types'; export const [getUnifiedDocViewerServices, setUnifiedDocViewerServices] = createGetterSetter('UnifiedDocViewerServices'); -const DocViewerLegacyTable = React.lazy(() => import('./components/doc_viewer_table/legacy')); -const DocViewerTable = React.lazy(() => import('./components/doc_viewer_table')); -const SourceViewer = React.lazy(() => import('./components/doc_viewer_source')); +const fallback = ( + + + +); + +const LazyDocViewerLogsOverview = dynamic(() => import('./components/doc_viewer_logs_overview'), { + fallback, +}); +const LazyDocViewerLegacyTable = dynamic(() => import('./components/doc_viewer_table/legacy'), { + fallback, +}); +const LazyDocViewerTable = dynamic(() => import('./components/doc_viewer_table'), { fallback }); +const LazySourceViewer = dynamic(() => import('./components/doc_viewer_source'), { fallback }); export interface UnifiedDocViewerSetup { registry: DocViewsRegistry; @@ -44,6 +56,18 @@ export class UnifiedDocViewerPublicPlugin private docViewsRegistry = new DocViewsRegistry(); public setup(core: CoreSetup) { + this.docViewsRegistry.add({ + id: 'doc_view_logs_overview', + title: i18n.translate('unifiedDocViewer.docViews.logsOverview.title', { + defaultMessage: 'Overview', + }), + order: 0, + enabled: false, // Disabled doc view by default, can be programmatically enabled using the DocViewsRegistry.prototype.enableById method. + component: (props) => { + return ; + }, + }); + this.docViewsRegistry.add({ id: 'doc_view_table', title: i18n.translate('unifiedDocViewer.docViews.table.tableTitle', { @@ -53,24 +77,15 @@ export class UnifiedDocViewerPublicPlugin component: (props) => { const { textBasedHits } = props; const { uiSettings } = getUnifiedDocViewerServices(); - const DocView = isLegacyTableEnabled({ + + const LazyDocView = isLegacyTableEnabled({ uiSettings, isTextBasedQueryMode: Array.isArray(textBasedHits), }) - ? DocViewerLegacyTable - : DocViewerTable; + ? LazyDocViewerLegacyTable + : LazyDocViewerTable; - return ( - - - - } - > - - - ); + return ; }, }); @@ -82,22 +97,14 @@ export class UnifiedDocViewerPublicPlugin order: 20, component: ({ hit, dataView, textBasedHits }) => { return ( - - - - } - > - {}} - /> - + {}} + /> ); }, }); diff --git a/src/plugins/unified_doc_viewer/tsconfig.json b/src/plugins/unified_doc_viewer/tsconfig.json index 07139b46059b88..1e641f1bce8914 100644 --- a/src/plugins/unified_doc_viewer/tsconfig.json +++ b/src/plugins/unified_doc_viewer/tsconfig.json @@ -25,7 +25,10 @@ "@kbn/core-ui-settings-browser-mocks", "@kbn/field-utils", "@kbn/code-editor", - "@kbn/code-editor-mock" + "@kbn/code-editor-mock", + "@kbn/custom-icons", + "@kbn/react-field", + "@kbn/ui-theme" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/observability_solution/logs_explorer/common/constants.ts b/x-pack/plugins/observability_solution/logs_explorer/common/constants.ts index aec0ad3b3adea9..ae8eff46fe7e2b 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/common/constants.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/common/constants.ts @@ -5,44 +5,12 @@ * 2.0. */ +import { fieldConstants } from '@kbn/discover-utils'; import { SmartFieldGridColumnOptions } from './display_options'; -export const LOGS_EXPLORER_PROFILE_ID = 'logs-explorer'; - -// Fields constants -export const TIMESTAMP_FIELD = '@timestamp'; -export const HOST_NAME_FIELD = 'host.name'; -export const LOG_LEVEL_FIELD = 'log.level'; -export const MESSAGE_FIELD = 'message'; -export const ERROR_MESSAGE_FIELD = 'error.message'; -export const EVENT_ORIGINAL_FIELD = 'event.original'; -export const TRACE_ID_FIELD = 'trace.id'; - -export const LOG_FILE_PATH_FIELD = 'log.file.path'; -export const DATASTREAM_NAMESPACE_FIELD = 'data_stream.namespace'; -export const DATASTREAM_DATASET_FIELD = 'data_stream.dataset'; +export * from '@kbn/discover-utils/src/field_constants'; -// Resource Fields -export const AGENT_NAME_FIELD = 'agent.name'; -export const CLOUD_PROVIDER_FIELD = 'cloud.provider'; -export const CLOUD_REGION_FIELD = 'cloud.region'; -export const CLOUD_AVAILABILITY_ZONE_FIELD = 'cloud.availability_zone'; -export const CLOUD_PROJECT_ID_FIELD = 'cloud.project.id'; -export const CLOUD_INSTANCE_ID_FIELD = 'cloud.instance.id'; -export const SERVICE_NAME_FIELD = 'service.name'; -export const ORCHESTRATOR_CLUSTER_NAME_FIELD = 'orchestrator.cluster.name'; -export const ORCHESTRATOR_RESOURCE_ID_FIELD = 'orchestrator.resource.id'; -export const ORCHESTRATOR_NAMESPACE_FIELD = 'orchestrator.namespace'; -export const CONTAINER_NAME_FIELD = 'container.name'; -export const CONTAINER_ID_FIELD = 'container.id'; - -// Degraded Docs -export const DEGRADED_DOCS_FIELD = 'ignored_field_values'; - -// Error Stacktrace -export const ERROR_STACK_TRACE = 'error.stack_trace'; -export const ERROR_EXCEPTION_STACKTRACE = 'error.exception.stacktrace'; -export const ERROR_LOG_STACKTRACE = 'error.log.stacktrace'; +export const LOGS_EXPLORER_PROFILE_ID = 'logs-explorer'; // Virtual column fields export const CONTENT_FIELD = 'content'; @@ -56,14 +24,14 @@ export const ACTIONS_COLUMN_WIDTH = 80; export const RESOURCE_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = { type: 'smart-field', smartField: RESOURCE_FIELD, - fallbackFields: [HOST_NAME_FIELD, SERVICE_NAME_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: [MESSAGE_FIELD], + fallbackFields: [fieldConstants.MESSAGE_FIELD], }; export const SMART_FALLBACK_FIELDS = { diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/flyout_detail.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/flyout_detail.tsx deleted file mode 100644 index 6beda5e75b0226..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/flyout_detail.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { LogsExplorerFlyoutContentProps } from '../../customizations/types'; -import { useDocDetail } from '../../hooks/use_doc_detail'; -import { FlyoutHeader } from './flyout_header'; -import { FlyoutHighlights } from './flyout_highlights'; -import { DiscoverActionsProvider } from '../../hooks/use_discover_action'; - -export function FlyoutDetail({ dataView, doc, actions }: LogsExplorerFlyoutContentProps) { - const parsedDoc = useDocDetail(doc, { dataView }); - - return ( - - - - - ); -} diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/flyout_header.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/flyout_header.tsx deleted file mode 100644 index b26f90145b6a52..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/flyout_header.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiAccordion, - useGeneratedHtmlId, - EuiTitle, -} from '@elastic/eui'; -import { FlyoutDoc } from '../../../common/document'; -import { getMessageWithFallbacks } from '../../hooks/use_doc_detail'; -import { LogLevel } from '../common/log_level'; -import { Timestamp } from './sub_components/timestamp'; -import * as constants from '../../../common/constants'; -import { flyoutContentLabel } from '../common/translations'; -import { HoverActionPopover } from './sub_components/hover_popover_action'; - -export function FlyoutHeader({ doc }: { doc: FlyoutDoc }) { - const hasTimestamp = Boolean(doc[constants.TIMESTAMP_FIELD]); - const hasLogLevel = Boolean(doc[constants.LOG_LEVEL_FIELD]); - const hasBadges = hasTimestamp || hasLogLevel; - const { field, value } = getMessageWithFallbacks(doc); - const hasMessageField = field && value; - const hasFlyoutHeader = hasMessageField || hasBadges; - - const accordionId = useGeneratedHtmlId({ - prefix: flyoutContentLabel, - }); - - const accordionTitle = ( - -

{flyoutContentLabel}

-
- ); - - const logLevelAndTimestamp = ( - - {hasBadges && ( - - {doc[constants.LOG_LEVEL_FIELD] && ( - - - - - - )} - {hasTimestamp && ( - - - - )} - - )} - - ); - - const contentField = hasMessageField && ( - - - - - - - {field} - - - {logLevelAndTimestamp} - - - - - - {value} - - - - - - ); - - return hasFlyoutHeader ? ( - - - {hasMessageField ? contentField : logLevelAndTimestamp} - - - ) : null; -} diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/flyout_highlights.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/flyout_highlights.tsx deleted file mode 100644 index 5e8af29566372f..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/flyout_highlights.tsx +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { CloudProvider, CloudProviderIcon } from '@kbn/custom-icons'; -import { useMeasure } from 'react-use/lib'; -import { first } from 'lodash'; -import { FlyoutDoc, LogDocument } from '../../../common/document'; -import * as constants from '../../../common/constants'; -import { HighlightField } from './sub_components/highlight_field'; -import { - cloudAccordionTitle, - flyoutCloudAvailabilityZoneLabel, - flyoutCloudInstanceIdLabel, - flyoutCloudProjectIdLabel, - flyoutCloudProviderLabel, - flyoutCloudRegionLabel, - flyoutDatasetLabel, - flyoutHostNameLabel, - flyoutLogPathFileLabel, - flyoutNamespaceLabel, - flyoutOrchestratorClusterNameLabel, - flyoutOrchestratorResourceIdLabel, - flyoutServiceLabel, - flyoutShipperLabel, - flyoutTraceLabel, - otherAccordionTitle, - serviceInfraAccordionTitle, -} from '../common/translations'; -import { HighlightSection } from './sub_components/highlight_section'; -import { HighlightContainer } from './sub_components/highlight_container'; -import { useFlyoutColumnWidth } from '../../hooks/use_flyouot_column_width'; - -export function FlyoutHighlights({ - formattedDoc, - flattenedDoc, -}: { - formattedDoc: FlyoutDoc; - flattenedDoc: LogDocument['flattened']; -}) { - const [ref, dimensions] = useMeasure(); - const { columns, fieldWidth } = useFlyoutColumnWidth(dimensions.width); - return ( - - {/* Service & Infrastructure highlight */} - - {formattedDoc[constants.SERVICE_NAME_FIELD] && ( - - )} - {formattedDoc[constants.HOST_NAME_FIELD] && ( - - )} - {formattedDoc[constants.TRACE_ID_FIELD] && ( - - )} - {formattedDoc[constants.ORCHESTRATOR_CLUSTER_NAME_FIELD] && ( - - )} - {formattedDoc[constants.ORCHESTRATOR_RESOURCE_ID_FIELD] && ( - - )} - - {/* Cloud highlight */} - - {formattedDoc[constants.CLOUD_PROVIDER_FIELD] && ( - - } - label={flyoutCloudProviderLabel} - value={flattenedDoc[constants.CLOUD_PROVIDER_FIELD]} - width={fieldWidth} - /> - )} - {formattedDoc[constants.CLOUD_REGION_FIELD] && ( - - )} - {formattedDoc[constants.CLOUD_AVAILABILITY_ZONE_FIELD] && ( - - )} - {formattedDoc[constants.CLOUD_PROJECT_ID_FIELD] && ( - - )} - {formattedDoc[constants.CLOUD_INSTANCE_ID_FIELD] && ( - - )} - - {/* Other highlights */} - - {formattedDoc[constants.LOG_FILE_PATH_FIELD] && ( - - )} - {formattedDoc[constants.DATASTREAM_DATASET_FIELD] && ( - - )} - {formattedDoc[constants.DATASTREAM_NAMESPACE_FIELD] && ( - - )} - {formattedDoc[constants.AGENT_NAME_FIELD] && ( - - )} - - - ); -} diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/index.ts b/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/index.ts deleted file mode 100644 index 78f7d3ac35c505..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './flyout_detail'; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_container.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_container.tsx deleted file mode 100644 index 4cef6f7b0850c6..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_container.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiHorizontalRule } from '@elastic/eui'; - -interface HighlightContainerProps { - children: React.ReactNode; -} - -const hasNonUndefinedSubChild = (children: React.ReactNode[]): boolean => { - return children.some((child) => { - if (React.isValidElement(child)) { - const subChildren = React.Children.toArray(child.props.children); - return subChildren.some((subChild) => subChild !== undefined && subChild !== null); - } - return false; - }); -}; - -export const HighlightContainer = React.forwardRef( - ({ children }, ref) => { - const validChildren = React.Children.toArray(children).filter(Boolean); - const hasChildren = validChildren.length > 0; - const shouldRender = hasChildren && hasNonUndefinedSubChild(validChildren); - - const flexChildren = validChildren.map((child, idx) =>
{child}
); - - return shouldRender ? ( -
- - {flexChildren} -
- ) : null; - } -); diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_field.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_field.tsx deleted file mode 100644 index 338643b3f302f8..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_field.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiTextTruncate, - EuiTitle, - useEuiTheme, -} from '@elastic/eui'; -import { css } from '@emotion/react'; -import React, { ReactNode } from 'react'; -import { dynamic } from '@kbn/shared-ux-utility'; -import { HoverActionPopover } from './hover_popover_action'; - -const HighlightFieldDescription = dynamic(() => import('./highlight_field_description')); - -interface HighlightFieldProps { - useBadge?: boolean; - field: string; - formattedValue: string; - icon?: ReactNode; - label: string; - value?: string; - width: number; -} - -export function HighlightField({ - useBadge = false, - field, - formattedValue, - icon, - label, - value, - width, - ...props -}: HighlightFieldProps) { - const { euiTheme } = useEuiTheme(); - - return formattedValue && value ? ( - - - - - - {label} - - - - - - - - - - - {icon && {icon}} - - - {(truncatedText: string) => - useBadge ? ( - {truncatedText} - ) : ( - - ) - } - - - - - - - ) : null; -} diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_section.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_section.tsx deleted file mode 100644 index 80260a6cff11c5..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/flyout_detail/sub_components/highlight_section.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { - EuiAccordion, - EuiFlexGrid, - EuiHorizontalRule, - EuiTitle, - EuiFlexItem, - useGeneratedHtmlId, - EuiButtonEmpty, -} from '@elastic/eui'; -import { flyoutAccordionShowMoreText } from '../../common/translations'; - -interface HighlightSectionProps { - title: string; - children: React.ReactNode; - columns: 1 | 2 | 3; -} - -const CHILDREN_PER_SECTION: 3 | 6 | 9 = 6; - -export function HighlightSection({ title, children, columns, ...props }: HighlightSectionProps) { - const validChildren = React.Children.toArray(children).filter(Boolean); - const childLength = validChildren.length; - const shouldRenderSection = childLength > 0; - const limitedChildren = validChildren.slice(0, CHILDREN_PER_SECTION - 1); - const [showMore, setShowMore] = useState(childLength > CHILDREN_PER_SECTION); - - const accordionId = useGeneratedHtmlId({ - prefix: title, - }); - - const hiddenCount = childLength - limitedChildren.length; - - const showMoreButtonLabel = flyoutAccordionShowMoreText(hiddenCount); - const showMoreButton = ( - { - setShowMore(false); - }} - > - {showMoreButtonLabel} - - ); - - limitedChildren.push(showMoreButton); - - const accordionTitle = ( - -

{title}

-
- ); - - const flexChildren = (showMore ? limitedChildren : validChildren).map((child, idx) => ( - {child} - )); - - return shouldRenderSection ? ( - <> - - - {flexChildren} - - - - - ) : null; -} diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/content.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/content.tsx index 073d18068d650c..b13cacec025f31 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/content.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/content.tsx @@ -10,12 +10,14 @@ import { css } from '@emotion/css'; import { EuiButtonIcon, EuiText } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; import type { DataGridCellValueElementProps } from '@kbn/unified-data-table'; -import { getShouldShowFieldHandler } from '@kbn/discover-utils'; +import { + getLogDocumentOverview, + getMessageFieldWithFallbacks, + getShouldShowFieldHandler, +} from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; import type { DataTableRecord } from '@kbn/discover-utils/src/types'; import { dynamic } from '@kbn/shared-ux-utility'; -import { useDocDetail, getMessageWithFallbacks } from '../../hooks/use_doc_detail'; -import { LogDocument } from '../../../common/document'; import { LogLevel } from '../common/log_level'; import * as constants from '../../../common/constants'; @@ -85,8 +87,8 @@ export const Content = ({ columnId, closePopover, }: DataGridCellValueElementProps) => { - const parsedDoc = useDocDetail(row as LogDocument, { dataView }); - const { field, value } = getMessageWithFallbacks(parsedDoc); + const documentOverview = getLogDocumentOverview(row, { dataView, fieldFormats }); + const { field, value } = getMessageFieldWithFallbacks(documentOverview); const renderLogMessage = field && value; const shouldShowFieldHandler = useMemo(() => { @@ -106,8 +108,8 @@ export const Content = ({ return ( - {parsedDoc[constants.LOG_LEVEL_FIELD] && ( - + {documentOverview[constants.LOG_LEVEL_FIELD] && ( + )} {renderLogMessage ? ( diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_flyout_content.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_flyout_content.tsx index 5f496fb8d189b0..9b60c5eabc5ae6 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_flyout_content.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_flyout_content.tsx @@ -8,7 +8,6 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; -import { FlyoutDetail } from '../components/flyout_detail/flyout_detail'; import { LogsExplorerFlyoutContentProps } from './types'; import { useLogsExplorerControllerContext } from '../controller'; import { LogDocument } from '../../common/document'; @@ -55,7 +54,8 @@ const CustomFlyoutContent = ({ const renderContent = ({ actions, dataView, doc }: LogsExplorerFlyoutContentProps) => ( - + {/* TOREMOVE */} + {/* */} ); diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx index 6935ac17fbff90..4b49e3f579f153 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx @@ -5,8 +5,6 @@ * 2.0. */ -/* eslint-disable react-hooks/rules-of-hooks */ - import React from 'react'; import type { CoreStart } from '@kbn/core/public'; import type { CustomizationCallback } from '@kbn/discover-plugin/public'; @@ -14,7 +12,6 @@ import { i18n } from '@kbn/i18n'; import { waitFor } from 'xstate/lib/waitFor'; import { dynamic } from '@kbn/shared-ux-utility'; import type { LogsExplorerController } from '../controller'; -import { LogsExplorerControllerProvider } from '../controller/provider'; import type { LogsExplorerStartDeps } from '../types'; import { useKibanaContextForPluginProvider } from '../utils/use_kibana'; import { createCustomSearchBar } from './custom_search_bar'; @@ -25,7 +22,6 @@ import { createCustomUnifiedHistogram } from './custom_unified_histogram'; const LazyCustomDataSourceFilters = dynamic(() => import('./custom_data_source_filters')); const LazyCustomDataSourceSelector = dynamic(() => import('./custom_data_source_selector')); -const LazyCustomFlyoutContent = dynamic(() => import('./custom_flyout_content')); export interface CreateLogsExplorerProfileCustomizationsDeps { core: CoreStart; @@ -120,7 +116,12 @@ export const createLogsExplorerProfileCustomizations = }); /** - * Hide flyout actions to prevent rendering hard-coded actions. + * Flyout customization. + * The latest changes moved the implementation of the flyout overview tab into the unified_doc_viewer presets. + * To keep control over the overview tab and enable it only on the Logs Explorer, + * the docViewsRegistry is updated to allow enable/disable of any doc view. + * In a close future, when the contextual awareness for Discover will be in place, + * this configuration will be moved into a flavored logs experience directly defined in Discover. */ customizations.set({ id: 'flyout', @@ -135,24 +136,7 @@ export const createLogsExplorerProfileCustomizations = }, }, docViewsRegistry: (registry) => { - registry.add({ - id: 'doc_view_log_overview', - title: i18n.translate('xpack.logsExplorer.flyoutDetail.docViews.overview', { - defaultMessage: 'Overview', - }), - order: 0, - component: (props) => { - const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider(core, plugins); - - return ( - - - - - - ); - }, - }); + registry.enableById('doc_view_logs_overview'); return registry; }, diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_discover_action.ts b/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_discover_action.ts deleted file mode 100644 index 68db3d6b932d63..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_discover_action.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import createContainer from 'constate'; -import type { LogsExplorerFlyoutContentProps } from '../customizations/types'; - -interface UseFlyoutActionsDeps { - value: LogsExplorerFlyoutContentProps['actions']; -} - -const useDiscoverActions = ({ value }: UseFlyoutActionsDeps) => value; - -export const [DiscoverActionsProvider, useDiscoverActionsContext] = - createContainer(useDiscoverActions); diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_doc_detail.ts b/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_doc_detail.ts deleted file mode 100644 index 6b59b0ac5e4f5b..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_doc_detail.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { formatFieldValue } from '@kbn/discover-utils'; -import * as constants from '../../common/constants'; -import { useKibanaContextForPlugin } from '../utils/use_kibana'; -import { LogsExplorerFlyoutContentProps } from '../customizations/types'; -import { FlyoutDoc, LogDocument } from '../../common/document'; - -export function useDocDetail( - doc: LogDocument, - { dataView }: Pick -): FlyoutDoc { - const { services } = useKibanaContextForPlugin(); - - const formatField = (field: F) => { - return ( - doc.flattened[field] && - formatFieldValue( - doc.flattened[field], - doc.raw, - services.fieldFormats, - dataView, - dataView.fields.getByName(field) - ) - ); - }; - - // Flyout Headers - const levelArray = doc.flattened[constants.LOG_LEVEL_FIELD]; - const level = levelArray && levelArray.length ? levelArray[0]?.toLowerCase() : undefined; - const messageArray = doc.flattened[constants.MESSAGE_FIELD]; - const message = messageArray && messageArray.length ? messageArray[0] : undefined; - const errorMessageArray = doc.flattened[constants.ERROR_MESSAGE_FIELD]; - const errorMessage = - errorMessageArray && errorMessageArray.length ? errorMessageArray[0] : undefined; - const eventOriginalArray = doc.flattened[constants.EVENT_ORIGINAL_FIELD]; - const eventOriginal = - eventOriginalArray && eventOriginalArray.length ? eventOriginalArray[0] : undefined; - const timestamp = formatField(constants.TIMESTAMP_FIELD); - - // Service Highlights - const serviceName = formatField(constants.SERVICE_NAME_FIELD); - const traceId = formatField(constants.TRACE_ID_FIELD); - - // Infrastructure Highlights - const hostname = formatField(constants.HOST_NAME_FIELD); - const orchestratorClusterName = formatField(constants.ORCHESTRATOR_CLUSTER_NAME_FIELD); - const orchestratorResourceId = formatField(constants.ORCHESTRATOR_RESOURCE_ID_FIELD); - - // Cloud Highlights - const cloudProvider = formatField(constants.CLOUD_PROVIDER_FIELD); - const cloudRegion = formatField(constants.CLOUD_REGION_FIELD); - const cloudAz = formatField(constants.CLOUD_AVAILABILITY_ZONE_FIELD); - const cloudProjectId = formatField(constants.CLOUD_PROJECT_ID_FIELD); - const cloudInstanceId = formatField(constants.CLOUD_INSTANCE_ID_FIELD); - - // Other Highlights - const logFilePath = formatField(constants.LOG_FILE_PATH_FIELD); - const namespace = formatField(constants.DATASTREAM_NAMESPACE_FIELD); - const dataset = formatField(constants.DATASTREAM_DATASET_FIELD); - const agentName = formatField(constants.AGENT_NAME_FIELD); - - return { - [constants.LOG_LEVEL_FIELD]: level, - [constants.TIMESTAMP_FIELD]: timestamp, - [constants.MESSAGE_FIELD]: message, - [constants.ERROR_MESSAGE_FIELD]: errorMessage, - [constants.EVENT_ORIGINAL_FIELD]: eventOriginal, - [constants.SERVICE_NAME_FIELD]: serviceName, - [constants.TRACE_ID_FIELD]: traceId, - [constants.HOST_NAME_FIELD]: hostname, - [constants.ORCHESTRATOR_CLUSTER_NAME_FIELD]: orchestratorClusterName, - [constants.ORCHESTRATOR_RESOURCE_ID_FIELD]: orchestratorResourceId, - [constants.CLOUD_PROVIDER_FIELD]: cloudProvider, - [constants.CLOUD_REGION_FIELD]: cloudRegion, - [constants.CLOUD_AVAILABILITY_ZONE_FIELD]: cloudAz, - [constants.CLOUD_PROJECT_ID_FIELD]: cloudProjectId, - [constants.CLOUD_INSTANCE_ID_FIELD]: cloudInstanceId, - [constants.LOG_FILE_PATH_FIELD]: logFilePath, - [constants.DATASTREAM_NAMESPACE_FIELD]: namespace, - [constants.DATASTREAM_DATASET_FIELD]: dataset, - [constants.AGENT_NAME_FIELD]: agentName, - }; -} - -export const getMessageWithFallbacks = (doc: FlyoutDoc) => { - const rankingOrder = [ - constants.MESSAGE_FIELD, - constants.ERROR_MESSAGE_FIELD, - constants.EVENT_ORIGINAL_FIELD, - ] as const; - - for (const rank of rankingOrder) { - if (doc[rank] !== undefined && doc[rank] !== null) { - return { field: rank, value: doc[rank] }; - } - } - - // If none of the ranks (fallbacks) are present - return { field: undefined }; -}; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_flyouot_column_width.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_flyouot_column_width.tsx deleted file mode 100644 index 1ea4250de49bfd..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_flyouot_column_width.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEuiTheme } from '@elastic/eui'; - -interface FlyoutColumnWidth { - columns: 1 | 2 | 3; - fieldWidth: number; -} - -export const useFlyoutColumnWidth = (width: number): FlyoutColumnWidth => { - const { euiTheme } = useEuiTheme(); - - const numberOfColumns = width > euiTheme.breakpoint.m ? 3 : width > euiTheme.breakpoint.s ? 2 : 1; - const WIDTH_FACTOR = 1.25; - const fieldWidth = width / (numberOfColumns * WIDTH_FACTOR); - - return { - columns: numberOfColumns, - fieldWidth, - }; -}; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_hover_actions.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_hover_actions.tsx deleted file mode 100644 index d8459215dc3669..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/hooks/use_hover_actions.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMemo, useState } from 'react'; -import { copyToClipboard, IconType } from '@elastic/eui'; -import { - flyoutHoverActionCopyToClipboardText, - flyoutHoverActionFilterForFieldPresentText, - actionFilterForText, - actionFilterOutText, - flyoutHoverActionToggleColumnText, -} from '../components/common/translations'; -import { useDiscoverActionsContext } from './use_discover_action'; - -interface HoverActionProps { - field: string; - value: string; -} - -export interface HoverActionType { - id: string; - tooltipContent: string; - iconType: IconType; - onClick: () => void; - display: boolean; -} - -export const useHoverActions = ({ field, value }: HoverActionProps): HoverActionType[] => { - const filterForText = actionFilterForText(value); - const filterOutText = actionFilterOutText(value); - const actions = useDiscoverActionsContext(); - const [columnAdded, setColumnAdded] = useState(false); - - return useMemo( - () => [ - { - id: 'addToFilterAction', - tooltipContent: filterForText, - iconType: 'plusInCircle', - onClick: () => actions?.addFilter && actions.addFilter(field, value, '+'), - display: true, - }, - { - id: 'removeFromFilterAction', - tooltipContent: filterOutText, - iconType: 'minusInCircle', - onClick: () => actions?.addFilter && actions.addFilter(field, value, '-'), - display: true, - }, - { - id: 'filterForFieldPresentAction', - tooltipContent: flyoutHoverActionFilterForFieldPresentText, - iconType: 'filter', - onClick: () => actions?.addFilter && actions.addFilter('_exists_', field, '+'), - display: true, - }, - { - id: 'toggleColumnAction', - tooltipContent: flyoutHoverActionToggleColumnText, - iconType: 'listAdd', - onClick: () => { - if (actions) { - if (columnAdded) { - actions?.removeColumn?.(field); - } else { - actions?.addColumn?.(field); - } - setColumnAdded(!columnAdded); - } - }, - display: true, - }, - { - id: 'copyToClipboardAction', - tooltipContent: flyoutHoverActionCopyToClipboardText, - iconType: 'copyClipboard', - onClick: () => copyToClipboard(value as string), - display: true, - }, - ], - [filterForText, filterOutText, actions, field, value, columnAdded] - ); -}; diff --git a/x-pack/plugins/observability_solution/logs_explorer/tsconfig.json b/x-pack/plugins/observability_solution/logs_explorer/tsconfig.json index e8c54c344ac51d..7c4224eadac192 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/tsconfig.json +++ b/x-pack/plugins/observability_solution/logs_explorer/tsconfig.json @@ -37,7 +37,6 @@ "@kbn/kibana-utils-plugin", "@kbn/management-settings-ids", "@kbn/navigation-plugin", - "@kbn/react-field", "@kbn/router-utils", "@kbn/share-plugin", "@kbn/shared-ux-utility", diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 5f7f2d3aa6bce9..9465c9e2d81708 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -24136,7 +24136,6 @@ "xpack.logsExplorer.flyoutDetail.accordion.title.cloud": "Cloud", "xpack.logsExplorer.flyoutDetail.accordion.title.other": "Autre", "xpack.logsExplorer.flyoutDetail.accordion.title.serviceInfra": "Service et Infrastructure", - "xpack.logsExplorer.flyoutDetail.docViews.overview": "Aperçu", "xpack.logsExplorer.flyoutDetail.label.cloudAvailabilityZone": "Zone de disponibilité du cloud", "xpack.logsExplorer.flyoutDetail.label.cloudInstanceId": "ID d'instance du cloud", "xpack.logsExplorer.flyoutDetail.label.cloudProjectId": "ID de projet du cloud", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 39b045f24542d0..e0b66c1a78933f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -24111,7 +24111,6 @@ "xpack.logsExplorer.flyoutDetail.accordion.title.cloud": "クラウド", "xpack.logsExplorer.flyoutDetail.accordion.title.other": "その他", "xpack.logsExplorer.flyoutDetail.accordion.title.serviceInfra": "サービスとインフラストラクチャー", - "xpack.logsExplorer.flyoutDetail.docViews.overview": "概要", "xpack.logsExplorer.flyoutDetail.label.cloudAvailabilityZone": "クラウドアベイラビリティゾーン", "xpack.logsExplorer.flyoutDetail.label.cloudInstanceId": "クラウドインスタンスID", "xpack.logsExplorer.flyoutDetail.label.cloudProjectId": "クラウドプロジェクトID", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cf8ba56c6bc8e6..bf9e41a6296e64 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -24144,7 +24144,6 @@ "xpack.logsExplorer.flyoutDetail.accordion.title.cloud": "云", "xpack.logsExplorer.flyoutDetail.accordion.title.other": "其他", "xpack.logsExplorer.flyoutDetail.accordion.title.serviceInfra": "服务和基础设施", - "xpack.logsExplorer.flyoutDetail.docViews.overview": "概览", "xpack.logsExplorer.flyoutDetail.label.cloudAvailabilityZone": "云可用区", "xpack.logsExplorer.flyoutDetail.label.cloudInstanceId": "云实例 ID", "xpack.logsExplorer.flyoutDetail.label.cloudProjectId": "云项目 ID", diff --git a/x-pack/test/functional/apps/observability_logs_explorer/flyout.ts b/x-pack/test/functional/apps/observability_logs_explorer/flyout.ts index d991626b18a5e7..22732d9933bbb3 100644 --- a/x-pack/test/functional/apps/observability_logs_explorer/flyout.ts +++ b/x-pack/test/functional/apps/observability_logs_explorer/flyout.ts @@ -65,34 +65,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } }); - it('should mount the flyout customization content', async () => { + it('should display the logs overview tab', async () => { await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutDetail'); - }); - - it('should display a timestamp badge', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutLogTimestamp'); - }); - - it('should display a log level badge when available', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutLogLevel'); - }); - - it('should not display a log level badge when not available', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); - await testSubjects.missingOrFail('logsExplorerFlyoutLogLevel'); - }); - - it('should display a message code block when available', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutLogMessage'); - }); - - it('should not display a message code block when not available', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); - await testSubjects.missingOrFail('logsExplorerFlyoutLogMessage'); + await testSubjects.existOrFail('docViewerTab-doc_view_logs_overview'); }); }); } diff --git a/x-pack/test/functional/apps/observability_logs_explorer/flyout_highlights.ts b/x-pack/test/functional/apps/observability_logs_explorer/flyout_highlights.ts deleted file mode 100644 index 238d456b5ec546..00000000000000 --- a/x-pack/test/functional/apps/observability_logs_explorer/flyout_highlights.ts +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { FtrProviderContext } from '../../ftr_provider_context'; - -const DATASET_NAME = 'flyout'; -const NAMESPACE = 'default'; -const DATA_STREAM_NAME = `logs-${DATASET_NAME}-${NAMESPACE}`; -const NOW = Date.now(); - -const sharedDoc = { - time: NOW + 1000, - logFilepath: '/flyout.log', - serviceName: 'frontend-node', - datasetName: DATASET_NAME, - namespace: NAMESPACE, - message: 'full document', - logLevel: 'info', - traceId: 'abcdef', - hostName: 'gke-edge-oblt-pool', - orchestratorClusterId: 'my-cluster-id', - orchestratorClusterName: 'my-cluster-id', - orchestratorResourceId: 'orchestratorResourceId', - cloudProvider: 'gcp', - cloudRegion: 'us-central-1', - cloudAz: 'us-central-1a', - cloudProjectId: 'elastic-project', - cloudInstanceId: 'BgfderflkjTheUiGuy', - agentName: 'node', -}; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const dataGrid = getService('dataGrid'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['observabilityLogsExplorer']); - - describe('Flyout highlight customization', () => { - let cleanupDataStreamSetup: () => Promise; - - describe('Service & Infrastructure container', () => { - const { - serviceName, - traceId, - hostName, - orchestratorClusterName, - orchestratorResourceId, - ...rest - } = sharedDoc; - const docWithoutServiceName = { - ...rest, - traceId, - hostName, - orchestratorClusterName, - orchestratorResourceId, - time: NOW - 1000, - }; - const docWithoutServiceInfraContainer = { ...rest, time: NOW - 4000 }; - - const docs = [sharedDoc, docWithoutServiceName, docWithoutServiceInfraContainer]; - before('setup DataStream', async () => { - cleanupDataStreamSetup = await PageObjects.observabilityLogsExplorer.setupDataStream( - DATASET_NAME, - NAMESPACE - ); - await PageObjects.observabilityLogsExplorer.ingestLogEntries(DATA_STREAM_NAME, docs); - }); - - after('clean up DataStream', async () => { - if (cleanupDataStreamSetup) { - await cleanupDataStreamSetup(); - } - }); - - beforeEach(async () => { - await PageObjects.observabilityLogsExplorer.navigateTo({ - pageState: { - time: { - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), - mode: 'absolute', - }, - }, - }); - }); - - it('should load the service container with all fields', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); - await testSubjects.existOrFail('logsExplorerFlyoutService'); - await testSubjects.existOrFail('logsExplorerFlyoutTrace'); - await testSubjects.existOrFail('logsExplorerFlyoutHostName'); - await testSubjects.existOrFail('logsExplorerFlyoutClusterName'); - await testSubjects.existOrFail('logsExplorerFlyoutResourceId'); - await dataGrid.closeFlyout(); - }); - - it('should load the service container even when 1 field is missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); - await testSubjects.missingOrFail('logsExplorerFlyoutService'); - await testSubjects.existOrFail('logsExplorerFlyoutTrace'); - await testSubjects.existOrFail('logsExplorerFlyoutHostName'); - await testSubjects.existOrFail('logsExplorerFlyoutClusterName'); - await testSubjects.existOrFail('logsExplorerFlyoutResourceId'); - await dataGrid.closeFlyout(); - }); - - it('should not load the service container if all fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 }); - await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); - await dataGrid.closeFlyout(); - }); - }); - - describe('Cloud container', () => { - const { cloudProvider, cloudInstanceId, cloudProjectId, cloudRegion, cloudAz, ...rest } = - sharedDoc; - const docWithoutCloudProviderAndInstanceId = { - ...rest, - cloudProjectId, - cloudRegion, - cloudAz, - time: NOW - 1000, - }; - const docWithoutCloudContainer = { ...rest, time: NOW - 2000 }; - - const docs = [sharedDoc, docWithoutCloudProviderAndInstanceId, docWithoutCloudContainer]; - before('setup DataStream', async () => { - cleanupDataStreamSetup = await PageObjects.observabilityLogsExplorer.setupDataStream( - DATASET_NAME, - NAMESPACE - ); - await PageObjects.observabilityLogsExplorer.ingestLogEntries(DATA_STREAM_NAME, docs); - }); - - after('clean up DataStream', async () => { - if (cleanupDataStreamSetup) { - await cleanupDataStreamSetup(); - } - }); - - beforeEach(async () => { - await PageObjects.observabilityLogsExplorer.navigateTo({ - pageState: { - time: { - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), - mode: 'absolute', - }, - }, - }); - }); - - it('should load the cloud container with all fields', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudProvider'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudRegion'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudAz'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudProjectId'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudInstanceId'); - await dataGrid.closeFlyout(); - }); - - it('should load the cloud container even when some fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud'); - - await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudInstanceId'); - - await testSubjects.existOrFail('logsExplorerFlyoutCloudRegion'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudAz'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudProjectId'); - await dataGrid.closeFlyout(); - }); - - it('should not load the cloud container if all fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 }); - await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionCloud'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudRegion'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudAz'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudProjectId'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudInstanceId'); - await dataGrid.closeFlyout(); - }); - }); - - describe('Other container', () => { - const { logFilepath, agentName, ...rest } = sharedDoc; - const docWithoutLogPathAndAgentName = { - ...rest, - time: NOW - 1000, - }; - - const docs = [sharedDoc, docWithoutLogPathAndAgentName]; - before('setup DataStream', async () => { - cleanupDataStreamSetup = await PageObjects.observabilityLogsExplorer.setupDataStream( - DATASET_NAME, - NAMESPACE - ); - await PageObjects.observabilityLogsExplorer.ingestLogEntries(DATA_STREAM_NAME, docs); - }); - - after('clean up DataStream', async () => { - if (cleanupDataStreamSetup) { - await cleanupDataStreamSetup(); - } - }); - - beforeEach(async () => { - await PageObjects.observabilityLogsExplorer.navigateTo({ - pageState: { - time: { - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), - mode: 'absolute', - }, - }, - }); - }); - - it('should load the other container with all fields', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther'); - await testSubjects.existOrFail('logsExplorerFlyoutLogPathFile'); - await testSubjects.existOrFail('logsExplorerFlyoutNamespace'); - await testSubjects.existOrFail('logsExplorerFlyoutDataset'); - await testSubjects.existOrFail('logsExplorerFlyoutLogShipper'); - await dataGrid.closeFlyout(); - }); - - it('should load the other container even when some fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther'); - - await testSubjects.missingOrFail('logsExplorerFlyoutLogPathFile'); - await testSubjects.missingOrFail('logsExplorerFlyoutLogShipper'); - - await testSubjects.existOrFail('logsExplorerFlyoutNamespace'); - await testSubjects.existOrFail('logsExplorerFlyoutDataset'); - await dataGrid.closeFlyout(); - }); - }); - }); -} diff --git a/x-pack/test/functional/apps/observability_logs_explorer/index.ts b/x-pack/test/functional/apps/observability_logs_explorer/index.ts index 640f552317f219..6683fb8d7d26d9 100644 --- a/x-pack/test/functional/apps/observability_logs_explorer/index.ts +++ b/x-pack/test/functional/apps/observability_logs_explorer/index.ts @@ -11,13 +11,12 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Observability Logs Explorer', function () { loadTestFile(require.resolve('./app')); loadTestFile(require.resolve('./columns_selection')); + loadTestFile(require.resolve('./custom_control_columns')); loadTestFile(require.resolve('./data_source_selection_state')); loadTestFile(require.resolve('./data_source_selector')); + loadTestFile(require.resolve('./field_list')); loadTestFile(require.resolve('./filter_controls')); loadTestFile(require.resolve('./flyout')); loadTestFile(require.resolve('./header_menu')); - loadTestFile(require.resolve('./flyout_highlights')); - loadTestFile(require.resolve('./custom_control_columns')); - loadTestFile(require.resolve('./field_list')); }); } diff --git a/x-pack/test/functional/page_objects/observability_logs_explorer.ts b/x-pack/test/functional/page_objects/observability_logs_explorer.ts index d78f38735cd7a1..832d4caf0e83e3 100644 --- a/x-pack/test/functional/page_objects/observability_logs_explorer.ts +++ b/x-pack/test/functional/page_objects/observability_logs_explorer.ts @@ -119,7 +119,6 @@ export function ObservabilityLogsExplorerPageObject({ getService, }: FtrProviderContext) { const PageObjects = getPageObjects(['common']); - const dataGrid = getService('dataGrid'); const es = getService('es'); const log = getService('log'); const queryBar = getService('queryBar'); @@ -335,11 +334,6 @@ export function ObservabilityLogsExplorerPageObject({ return testSubjects.find('unmanagedDatasets'); }, - async getFlyoutDetail(rowIndex: number = 0) { - await dataGrid.clickRowToggle({ rowIndex }); - return testSubjects.find('logsExplorerFlyoutDetail'); - }, - async getIntegrations() { const menu = await this.getIntegrationsContextMenu(); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout.ts index 918d710f8b3c0d..8758ca590a5158 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout.ts @@ -67,34 +67,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } }); - it('should mount the flyout customization content', async () => { + it('should display the logs overview tab', async () => { await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutDetail'); - }); - - it('should display a timestamp badge', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutLogTimestamp'); - }); - - it('should display a log level badge when available', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutLogLevel'); - }); - - it('should not display a log level badge when not available', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); - await testSubjects.missingOrFail('logsExplorerFlyoutLogLevel'); - }); - - it('should display a message code block when available', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutLogMessage'); - }); - - it('should not display a message code block when not available', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); - await testSubjects.missingOrFail('logsExplorerFlyoutLogMessage'); + await testSubjects.existOrFail('docViewerTab-doc_view_logs_overview'); }); }); } diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout_highlights.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout_highlights.ts deleted file mode 100644 index b143d59e96cadd..00000000000000 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout_highlights.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { FtrProviderContext } from '../../../ftr_provider_context'; - -const DATASET_NAME = 'flyout'; -const NAMESPACE = 'default'; -const DATA_STREAM_NAME = `logs-${DATASET_NAME}-${NAMESPACE}`; -const NOW = Date.now(); - -const sharedDoc = { - time: NOW + 1000, - logFilepath: '/flyout.log', - serviceName: 'frontend-node', - datasetName: DATASET_NAME, - namespace: NAMESPACE, - message: 'full document', - logLevel: 'info', - traceId: 'abcdef', - hostName: 'gke-edge-oblt-pool', - orchestratorClusterId: 'my-cluster-id', - orchestratorClusterName: 'my-cluster-id', - orchestratorResourceId: 'orchestratorResourceId', - cloudProvider: 'gcp', - cloudRegion: 'us-central-1', - cloudAz: 'us-central-1a', - cloudProjectId: 'elastic-project', - cloudInstanceId: 'BgfderflkjTheUiGuy', - agentName: 'node', -}; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const dataGrid = getService('dataGrid'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['observabilityLogsExplorer', 'svlCommonPage']); - - describe('Flyout highlight customization', () => { - let cleanupDataStreamSetup: () => Promise; - - describe('Service & Infrastructure container', () => { - const { - serviceName, - traceId, - hostName, - orchestratorClusterName, - orchestratorResourceId, - ...rest - } = sharedDoc; - const docWithoutServiceName = { - ...rest, - traceId, - hostName, - orchestratorClusterName, - orchestratorResourceId, - time: NOW - 1000, - }; - const docWithoutServiceInfraContainer = { ...rest, time: NOW - 4000 }; - - const docs = [sharedDoc, docWithoutServiceName, docWithoutServiceInfraContainer]; - before('setup DataStream', async () => { - cleanupDataStreamSetup = await PageObjects.observabilityLogsExplorer.setupDataStream( - DATASET_NAME, - NAMESPACE - ); - await PageObjects.observabilityLogsExplorer.ingestLogEntries(DATA_STREAM_NAME, docs); - await PageObjects.svlCommonPage.login(); - }); - - after('clean up DataStream', async () => { - await PageObjects.svlCommonPage.forceLogout(); - if (cleanupDataStreamSetup) { - await cleanupDataStreamSetup(); - } - }); - - beforeEach(async () => { - await PageObjects.observabilityLogsExplorer.navigateTo({ - pageState: { - time: { - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), - mode: 'absolute', - }, - }, - }); - }); - - it('should load the service container with all fields', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); - await testSubjects.existOrFail('logsExplorerFlyoutService'); - await testSubjects.existOrFail('logsExplorerFlyoutTrace'); - await testSubjects.existOrFail('logsExplorerFlyoutHostName'); - await testSubjects.existOrFail('logsExplorerFlyoutClusterName'); - await testSubjects.existOrFail('logsExplorerFlyoutResourceId'); - await dataGrid.closeFlyout(); - }); - - it('should load the service container even when 1 field is missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); - await testSubjects.missingOrFail('logsExplorerFlyoutService'); - await testSubjects.existOrFail('logsExplorerFlyoutTrace'); - await testSubjects.existOrFail('logsExplorerFlyoutHostName'); - await testSubjects.existOrFail('logsExplorerFlyoutClusterName'); - await testSubjects.existOrFail('logsExplorerFlyoutResourceId'); - await dataGrid.closeFlyout(); - }); - - it('should not load the service container if all fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 }); - await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); - await dataGrid.closeFlyout(); - }); - }); - - describe('Cloud container', () => { - const { cloudProvider, cloudInstanceId, cloudProjectId, cloudRegion, cloudAz, ...rest } = - sharedDoc; - const docWithoutCloudProviderAndInstanceId = { - ...rest, - cloudProjectId, - cloudRegion, - cloudAz, - time: NOW - 1000, - }; - const docWithoutCloudContainer = { ...rest, time: NOW - 2000 }; - - const docs = [sharedDoc, docWithoutCloudProviderAndInstanceId, docWithoutCloudContainer]; - before('setup DataStream', async () => { - cleanupDataStreamSetup = await PageObjects.observabilityLogsExplorer.setupDataStream( - DATASET_NAME, - NAMESPACE - ); - await PageObjects.observabilityLogsExplorer.ingestLogEntries(DATA_STREAM_NAME, docs); - await PageObjects.svlCommonPage.login(); - }); - - after('clean up DataStream', async () => { - await PageObjects.svlCommonPage.forceLogout(); - if (cleanupDataStreamSetup) { - await cleanupDataStreamSetup(); - } - }); - - beforeEach(async () => { - await PageObjects.observabilityLogsExplorer.navigateTo({ - pageState: { - time: { - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), - mode: 'absolute', - }, - }, - }); - }); - - it('should load the cloud container with all fields', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudProvider'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudRegion'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudAz'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudProjectId'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudInstanceId'); - await dataGrid.closeFlyout(); - }); - - it('should load the cloud container even when some fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud'); - - await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudInstanceId'); - - await testSubjects.existOrFail('logsExplorerFlyoutCloudRegion'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudAz'); - await testSubjects.existOrFail('logsExplorerFlyoutCloudProjectId'); - await dataGrid.closeFlyout(); - }); - - it('should not load the cloud container if all fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 }); - await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionCloud'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudRegion'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudAz'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudProjectId'); - await testSubjects.missingOrFail('logsExplorerFlyoutCloudInstanceId'); - await dataGrid.closeFlyout(); - }); - }); - - describe('Other container', () => { - const { logFilepath, agentName, ...rest } = sharedDoc; - const docWithoutLogPathAndAgentName = { - ...rest, - time: NOW - 1000, - }; - - const docs = [sharedDoc, docWithoutLogPathAndAgentName]; - before('setup DataStream', async () => { - cleanupDataStreamSetup = await PageObjects.observabilityLogsExplorer.setupDataStream( - DATASET_NAME, - NAMESPACE - ); - await PageObjects.observabilityLogsExplorer.ingestLogEntries(DATA_STREAM_NAME, docs); - await PageObjects.svlCommonPage.login(); - }); - - after('clean up DataStream', async () => { - await PageObjects.svlCommonPage.forceLogout(); - if (cleanupDataStreamSetup) { - await cleanupDataStreamSetup(); - } - }); - - beforeEach(async () => { - await PageObjects.observabilityLogsExplorer.navigateTo({ - pageState: { - time: { - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), - mode: 'absolute', - }, - }, - }); - }); - - it('should load the other container with all fields', async () => { - await dataGrid.clickRowToggle({ columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther'); - await testSubjects.existOrFail('logsExplorerFlyoutLogPathFile'); - await testSubjects.existOrFail('logsExplorerFlyoutNamespace'); - await testSubjects.existOrFail('logsExplorerFlyoutDataset'); - await testSubjects.existOrFail('logsExplorerFlyoutLogShipper'); - await dataGrid.closeFlyout(); - }); - - it('should load the other container even when some fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); - await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther'); - - await testSubjects.missingOrFail('logsExplorerFlyoutLogPathFile'); - await testSubjects.missingOrFail('logsExplorerFlyoutLogShipper'); - - await testSubjects.existOrFail('logsExplorerFlyoutNamespace'); - await testSubjects.existOrFail('logsExplorerFlyoutDataset'); - await dataGrid.closeFlyout(); - }); - }); - }); -} diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/index.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/index.ts index e445761dd6c19b..1f49779be0f27e 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/index.ts @@ -11,13 +11,12 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Observability Logs Explorer', function () { loadTestFile(require.resolve('./app')); loadTestFile(require.resolve('./columns_selection')); + loadTestFile(require.resolve('./custom_control_columns')); loadTestFile(require.resolve('./data_source_selection_state')); loadTestFile(require.resolve('./data_source_selector')); + loadTestFile(require.resolve('./field_list')); loadTestFile(require.resolve('./filter_controls')); loadTestFile(require.resolve('./flyout')); loadTestFile(require.resolve('./header_menu')); - loadTestFile(require.resolve('./flyout_highlights')); - loadTestFile(require.resolve('./custom_control_columns')); - loadTestFile(require.resolve('./field_list')); }); }