diff --git a/src/plugins/discover/public/customizations/customization_types/flyout_customization.ts b/src/plugins/discover/public/customizations/customization_types/flyout_customization.ts index 08f8f034a00d3..84f3f27715039 100644 --- a/src/plugins/discover/public/customizations/customization_types/flyout_customization.ts +++ b/src/plugins/discover/public/customizations/customization_types/flyout_customization.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import type { DataTableRecord } from '@kbn/discover-utils/types'; import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import React, { type ComponentType } from 'react'; diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 42033c2ebfdb5..37a46f30c4ccf 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -26,6 +26,8 @@ export type { DiscoverCustomization, DiscoverCustomizationService, FlyoutCustomization, + FlyoutContentActions, + FlyoutContentProps, SearchBarCustomization, UnifiedHistogramCustomization, TopNavCustomization, diff --git a/x-pack/plugins/log_explorer/kibana.jsonc b/x-pack/plugins/log_explorer/kibana.jsonc index 969fa50c87a8c..76eb47e4a5915 100644 --- a/x-pack/plugins/log_explorer/kibana.jsonc +++ b/x-pack/plugins/log_explorer/kibana.jsonc @@ -15,6 +15,7 @@ "data", "dataViews", "discover", + "fieldFormats", "fleet", "kibanaReact", "kibanaUtils", diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx new file mode 100644 index 0000000000000..012e5c914ed61 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx @@ -0,0 +1,47 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { LogLevel } from './sub_components/log_level'; +import { Timestamp } from './sub_components/timestamp'; +import { FlyoutProps, LogDocument } from './types'; +import { getDocDetailRenderFlags, useDocDetail } from './use_doc_detail'; +import { Message } from './sub_components/message'; + +export function FlyoutDetail({ dataView, doc }: Pick) { + const parsedDoc = useDocDetail(doc as LogDocument, { dataView }); + + const { hasTimestamp, hasLogLevel, hasMessage, hasBadges, hasFlyoutHeader } = + getDocDetailRenderFlags(parsedDoc); + + return hasFlyoutHeader ? ( + + + {hasBadges && ( + + {hasLogLevel && ( + + + + )} + {hasTimestamp && ( + + + + )} + + )} + + {hasMessage && ( + + + + )} + + ) : null; +} diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/index.ts b/x-pack/plugins/log_explorer/public/components/flyout_detail/index.ts new file mode 100644 index 0000000000000..4b00c914df228 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/index.ts @@ -0,0 +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. + */ + +export * from './flyout_detail'; +export * from './types'; diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/log_level.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/log_level.tsx new file mode 100644 index 0000000000000..57858bb8c0819 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/log_level.tsx @@ -0,0 +1,33 @@ +/* + * 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 { EuiBadge, type EuiBadgeProps } from '@elastic/eui'; +import { FlyoutDoc } from '../types'; + +const LEVEL_DICT: Record = { + error: 'danger', + warn: 'warning', + info: 'primary', + default: 'default', +}; + +interface LogLevelProps { + level: FlyoutDoc['log.level']; +} + +export function LogLevel({ level }: LogLevelProps) { + if (!level) return null; + + const levelColor = LEVEL_DICT[level] ?? LEVEL_DICT.default; + + return ( + + {level} + + ); +} diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/message.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/message.tsx new file mode 100644 index 0000000000000..584f2eeff5a50 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/message.tsx @@ -0,0 +1,34 @@ +/* + * 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 } from '@elastic/eui'; +import { FlyoutDoc } from '../types'; +import { flyoutMessageLabel } from '../translations'; + +interface MessageProps { + message: FlyoutDoc['message']; +} + +export function Message({ message }: MessageProps) { + if (!message) return null; + + return ( + + + + {flyoutMessageLabel} + + + + + {message} + + + + ); +} diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/timestamp.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/timestamp.tsx new file mode 100644 index 0000000000000..95df948c80560 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/timestamp.tsx @@ -0,0 +1,24 @@ +/* + * 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 { EuiBadge } from '@elastic/eui'; +import { FlyoutDoc } from '../types'; + +interface TimestampProps { + timestamp: FlyoutDoc['@timestamp']; +} + +export function Timestamp({ timestamp }: TimestampProps) { + if (!timestamp) return null; + + return ( + + {timestamp} + + ); +} diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/translations.ts b/x-pack/plugins/log_explorer/public/components/flyout_detail/translations.ts new file mode 100644 index 0000000000000..fcb42cf79a5dd --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/translations.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const flyoutMessageLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.message', { + defaultMessage: 'Message', +}); diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts b/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts new file mode 100644 index 0000000000000..cf8cfb8170e21 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EuiIconType } from '@elastic/eui/src/components/icon/icon'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { FlyoutContentProps } from '@kbn/discover-plugin/public'; +import type { DataTableRecord } from '@kbn/discover-utils/types'; + +export interface FlyoutProps extends FlyoutContentProps { + dataView: DataView; +} + +export interface LogDocument extends DataTableRecord { + flattened: { + '@timestamp': string; + 'log.level'?: string; + message?: string; + }; +} + +export interface FlyoutDoc { + '@timestamp': string; + 'log.level'?: string; + message?: string; +} + +export interface FlyoutHighlightField { + label: string; + value: string; + iconType?: EuiIconType; +} diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts b/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts new file mode 100644 index 0000000000000..32e4bcd966745 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts @@ -0,0 +1,60 @@ +/* + * 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 { LOG_LEVEL_FIELD, MESSAGE_FIELD, TIMESTAMP_FIELD } from '../../../common/constants'; +import { useKibanaContextForPlugin } from '../../utils/use_kibana'; +import { FlyoutDoc, FlyoutProps, LogDocument } from './types'; + +export function useDocDetail( + doc: LogDocument, + { dataView }: Pick +): FlyoutDoc { + const { services } = useKibanaContextForPlugin(); + + const formatField = ( + field: F + ): LogDocument['flattened'][F] => { + return ( + doc.flattened[field] && + formatFieldValue( + doc.flattened[field], + doc.raw, + services.fieldFormats, + dataView, + dataView.fields.getByName(field) + ) + ); + }; + + const level = formatField(LOG_LEVEL_FIELD)?.toLowerCase(); + const timestamp = formatField(TIMESTAMP_FIELD); + const message = formatField(MESSAGE_FIELD); + + return { + [LOG_LEVEL_FIELD]: level, + [TIMESTAMP_FIELD]: timestamp, + [MESSAGE_FIELD]: message, + }; +} + +export const getDocDetailRenderFlags = (doc: FlyoutDoc) => { + const hasTimestamp = Boolean(doc['@timestamp']); + const hasLogLevel = Boolean(doc['log.level']); + const hasMessage = Boolean(doc.message); + + const hasBadges = hasTimestamp || hasLogLevel; + + const hasFlyoutHeader = hasBadges || hasMessage; + + return { + hasTimestamp, + hasLogLevel, + hasMessage, + hasBadges, + hasFlyoutHeader, + }; +}; diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx new file mode 100644 index 0000000000000..a4b473119744a --- /dev/null +++ b/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx @@ -0,0 +1,32 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FlyoutDetail } from '../components/flyout_detail/flyout_detail'; +import { FlyoutProps } from '../components/flyout_detail'; + +export const CustomFlyoutContent = ({ + actions, + dataView, + doc, + renderDefaultContent, +}: FlyoutProps) => { + return ( + + {/* Apply custom Log Explorer detail */} + + + + {/* Restore default content */} + {renderDefaultContent()} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default CustomFlyoutContent; diff --git a/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx b/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx index 29a3744a9484f..85d1284752977 100644 --- a/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx @@ -8,14 +8,16 @@ import type { CoreStart } from '@kbn/core/public'; import { CustomizationCallback, DiscoverStateContainer } from '@kbn/discover-plugin/public'; import React from 'react'; import { type BehaviorSubject, combineLatest, from, map, Subscription } from 'rxjs'; +import useObservable from 'react-use/lib/useObservable'; import { dynamic } from '../utils/dynamic'; import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; import { LogExplorerStateContainer } from '../components/log_explorer'; import { LogExplorerStartDeps } from '../types'; import { useKibanaContextForPluginProvider } from '../utils/use_kibana'; -const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector')); const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters')); +const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector')); +const LazyCustomFlyoutContent = dynamic(() => import('./custom_flyout_content')); export interface CreateLogExplorerProfileCustomizationsDeps { core: CoreStart; @@ -115,6 +117,20 @@ export const createLogExplorerProfileCustomizations = viewSurroundingDocument: { disabled: true }, }, }, + Content: (props) => { + const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider(core, plugins); + + const internalState = useObservable( + stateContainer.internalState.state$, + stateContainer.internalState.get() + ); + + return ( + + + + ); + }, }); return () => { diff --git a/x-pack/plugins/log_explorer/public/types.ts b/x-pack/plugins/log_explorer/public/types.ts index b260f6ba68ad7..e07b8fdb14b3f 100644 --- a/x-pack/plugins/log_explorer/public/types.ts +++ b/x-pack/plugins/log_explorer/public/types.ts @@ -9,6 +9,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public'; import { SharePluginSetup } from '@kbn/share-plugin/public'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { LogExplorerLocators } from '../common/locators'; import type { LogExplorerProps } from './components/log_explorer'; @@ -28,4 +29,5 @@ export interface LogExplorerStartDeps { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; discover: DiscoverStart; + fieldFormats: FieldFormatsStart; } diff --git a/x-pack/plugins/log_explorer/tsconfig.json b/x-pack/plugins/log_explorer/tsconfig.json index 13a82d1da9f4c..d80736004536f 100644 --- a/x-pack/plugins/log_explorer/tsconfig.json +++ b/x-pack/plugins/log_explorer/tsconfig.json @@ -24,7 +24,8 @@ "@kbn/unified-data-table", "@kbn/core-ui-settings-browser", "@kbn/discover-utils", - "@kbn/deeplinks-observability" + "@kbn/deeplinks-observability", + "@kbn/field-formats-plugin" ], "exclude": ["target/**/*"] } diff --git a/x-pack/test/functional/apps/observability_log_explorer/flyout.ts b/x-pack/test/functional/apps/observability_log_explorer/flyout.ts new file mode 100644 index 0000000000000..f8277db16e8db --- /dev/null +++ b/x-pack/test/functional/apps/observability_log_explorer/flyout.ts @@ -0,0 +1,91 @@ +/* + * 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 = { + logFilepath: '/flyout.log', + serviceName: DATASET_NAME, + datasetName: DATASET_NAME, + namespace: NAMESPACE, +}; + +const docs = [ + { + ...sharedDoc, + time: NOW + 1000, + message: 'full document', + logLevel: 'info', + }, + { + ...sharedDoc, + time: NOW, + }, +]; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dataGrid = getService('dataGrid'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['observabilityLogExplorer']); + + describe('Flyout content customization', () => { + let cleanupDataStreamSetup: () => Promise; + + before('initialize tests', async () => { + cleanupDataStreamSetup = await PageObjects.observabilityLogExplorer.setupDataStream( + DATASET_NAME, + NAMESPACE + ); + await PageObjects.observabilityLogExplorer.ingestLogEntries(DATA_STREAM_NAME, docs); + }); + + beforeEach(async () => { + await PageObjects.observabilityLogExplorer.navigateTo({ + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + }); + }); + + after('clean up archives', async () => { + if (cleanupDataStreamSetup) { + cleanupDataStreamSetup(); + } + }); + + it('should mount the flyout customization content', async () => { + await dataGrid.clickRowToggle(); + await testSubjects.existOrFail('logExplorerFlyoutDetail'); + }); + + it('should display a timestamp badge', async () => { + await dataGrid.clickRowToggle(); + await testSubjects.existOrFail('logExplorerFlyoutLogTimestamp'); + }); + + it('should display a log level badge when available', async () => { + await dataGrid.clickRowToggle(); + await testSubjects.existOrFail('logExplorerFlyoutLogLevel'); + await dataGrid.closeFlyout(); + + await dataGrid.clickRowToggle({ rowIndex: 1 }); + await testSubjects.missingOrFail('logExplorerFlyoutLogLevel'); + }); + + it('should display a message code block when available', async () => { + await dataGrid.clickRowToggle(); + await testSubjects.existOrFail('logExplorerFlyoutLogMessage'); + await dataGrid.closeFlyout(); + + await dataGrid.clickRowToggle({ rowIndex: 1 }); + await testSubjects.missingOrFail('logExplorerFlyoutLogMessage'); + }); + }); +} diff --git a/x-pack/test/functional/apps/observability_log_explorer/index.ts b/x-pack/test/functional/apps/observability_log_explorer/index.ts index aec38a6bb8308..948910dab6ab4 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/index.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/index.ts @@ -14,6 +14,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dataset_selection_state')); loadTestFile(require.resolve('./dataset_selector')); loadTestFile(require.resolve('./filter_controls')); + loadTestFile(require.resolve('./flyout')); loadTestFile(require.resolve('./header_menu')); }); } diff --git a/x-pack/test/functional/page_objects/observability_log_explorer.ts b/x-pack/test/functional/page_objects/observability_log_explorer.ts index 15da13b99c70f..1804ae95ddc92 100644 --- a/x-pack/test/functional/page_objects/observability_log_explorer.ts +++ b/x-pack/test/functional/page_objects/observability_log_explorer.ts @@ -109,7 +109,10 @@ export function ObservabilityLogExplorerPageObject({ getService, }: FtrProviderContext) { const PageObjects = getPageObjects(['common']); + const dataGrid = getService('dataGrid'); + const es = getService('es'); const log = getService('log'); + const queryBar = getService('queryBar'); const supertest = getService('supertest'); const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); @@ -119,6 +122,8 @@ export function ObservabilityLogExplorerPageObject({ 'search' > & { search?: Record; + from?: string; + to?: string; }; return { @@ -167,6 +172,27 @@ export function ObservabilityLogExplorerPageObject({ }; }, + async setupDataStream(datasetName: string, namespace: string = 'default') { + const dataStream = `logs-${datasetName}-${namespace}`; + log.info(`===== Setup initial data stream "${dataStream}". =====`); + await es.indices.createDataStream({ name: dataStream }); + + return async () => { + log.info(`===== Removing data stream "${dataStream}". =====`); + await es.indices.deleteDataStream({ + name: dataStream, + }); + }; + }, + + ingestLogEntries(dataStream: string, docs: MockLogDoc[] = []) { + log.info(`===== Ingesting ${docs.length} docs for "${dataStream}" data stream. =====`); + return es.bulk({ + body: docs.flatMap((doc) => [{ create: { _index: dataStream } }, createLogDoc(doc)]), + refresh: 'wait_for', + }); + }, + async setupAdditionalIntegrations() { log.info(`===== Setup additional integration packages. =====`); log.info(`===== Install ${additionalPackages.length} mock integration packages. =====`); @@ -183,11 +209,11 @@ export function ObservabilityLogExplorerPageObject({ }, async navigateTo(options: NavigateToAppOptions = {}) { - const { search = {}, ...extraOptions } = options; + const { search = {}, from = FROM, to = TO, ...extraOptions } = options; const composedSearch = querystring.stringify({ ...search, _g: rison.encode({ - time: { from: FROM, to: TO }, + time: { from, to }, }), }); @@ -262,6 +288,11 @@ export function ObservabilityLogExplorerPageObject({ return testSubjects.find('unmanagedDatasets'); }, + async getFlyoutDetail(rowIndex: number = 0) { + await dataGrid.clickRowToggle({ rowIndex }); + return testSubjects.find('logExplorerFlyoutDetail'); + }, + async getIntegrations() { const menu = await this.getIntegrationsContextMenu(); @@ -359,24 +390,65 @@ export function ObservabilityLogExplorerPageObject({ }, // Query Bar - getQueryBar() { - return testSubjects.find('queryInput'); + getQueryBarValue() { + return queryBar.getQueryString(); }, - async getQueryBarValue() { - const queryBar = await testSubjects.find('queryInput'); - return queryBar.getAttribute('value'); + async submitQuery(query: string) { + await queryBar.setQuery(query); + await queryBar.clickQuerySubmitButton(); }, + }; +} - async typeInQueryBar(query: string) { - const queryBar = await this.getQueryBar(); - await queryBar.clearValueWithKeyboard(); - return queryBar.type(query); - }, +interface MockLogDoc { + time: number; + logFilepath: string; + serviceName?: string; + namespace: string; + datasetName: string; + message?: string; + logLevel?: string; + [key: string]: unknown; +} - async submitQuery(query: string) { - await this.typeInQueryBar(query); - await testSubjects.click('querySubmitButton'); - }, +export function createLogDoc({ + time, + logFilepath, + serviceName, + namespace, + datasetName, + message, + logLevel, + ...extraFields +}: MockLogDoc) { + return { + input: { + type: 'log', + }, + '@timestamp': new Date(time).toISOString(), + log: { + file: { + path: logFilepath, + }, + }, + ...(serviceName + ? { + service: { + name: serviceName, + }, + } + : {}), + data_stream: { + namespace, + type: 'logs', + dataset: datasetName, + }, + message, + event: { + dataset: datasetName, + }, + ...(logLevel && { 'log.level': logLevel }), + ...extraFields, }; } diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts new file mode 100644 index 0000000000000..79f241c8948b6 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts @@ -0,0 +1,93 @@ +/* + * 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 = { + logFilepath: '/flyout.log', + serviceName: DATASET_NAME, + datasetName: DATASET_NAME, + namespace: NAMESPACE, +}; + +const docs = [ + { + ...sharedDoc, + time: NOW + 1000, + message: 'full document', + logLevel: 'info', + }, + { + ...sharedDoc, + time: NOW, + }, +]; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dataGrid = getService('dataGrid'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['observabilityLogExplorer', 'svlCommonPage']); + + describe('Flyout content customization', () => { + let cleanupDataStreamSetup: () => Promise; + + before('initialize tests', async () => { + cleanupDataStreamSetup = await PageObjects.observabilityLogExplorer.setupDataStream( + DATASET_NAME, + NAMESPACE + ); + await PageObjects.observabilityLogExplorer.ingestLogEntries(DATA_STREAM_NAME, docs); + await PageObjects.svlCommonPage.login(); + }); + + beforeEach(async () => { + await PageObjects.observabilityLogExplorer.navigateTo({ + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + }); + }); + + after('clean up archives', async () => { + await PageObjects.svlCommonPage.forceLogout(); + if (cleanupDataStreamSetup) { + cleanupDataStreamSetup(); + } + }); + + it('should mount the flyout customization content', async () => { + await dataGrid.clickRowToggle(); + await testSubjects.existOrFail('logExplorerFlyoutDetail'); + }); + + it('should display a timestamp badge', async () => { + await dataGrid.clickRowToggle(); + await testSubjects.existOrFail('logExplorerFlyoutLogTimestamp'); + }); + + it('should display a log level badge when available', async () => { + await dataGrid.clickRowToggle(); + await testSubjects.existOrFail('logExplorerFlyoutLogLevel'); + await dataGrid.closeFlyout(); + + await dataGrid.clickRowToggle({ rowIndex: 1 }); + await testSubjects.missingOrFail('logExplorerFlyoutLogLevel'); + }); + + it('should display a message code block when available', async () => { + await dataGrid.clickRowToggle(); + await testSubjects.existOrFail('logExplorerFlyoutLogMessage'); + await dataGrid.closeFlyout(); + + await dataGrid.clickRowToggle({ rowIndex: 1 }); + await testSubjects.missingOrFail('logExplorerFlyoutLogMessage'); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts index 77f89dad01f77..6e0b650869680 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts @@ -14,6 +14,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dataset_selection_state')); loadTestFile(require.resolve('./dataset_selector')); loadTestFile(require.resolve('./filter_controls')); + loadTestFile(require.resolve('./flyout')); loadTestFile(require.resolve('./header_menu')); }); }