mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Log Explorer] Implement Flyout content header (#169832)
## 📓 Summary
Closes #169501
🛑 ~**Merge blocked by:** https://github.com/elastic/kibana/pull/169634~
This work implements the first frame for a detailed log flyout.
It adds highlight on the log level, timestamp and message details for a
log.
This first layer of customization will work as a base for all the
upcoming enhancements on the flyout detail.
a1c2997c
-5fef-4899-836f-ff810de3f148
---------
Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Achyut Jhunjhunwala <achyut.jhunjhunwala@elastic.co>
This commit is contained in:
parent
014df2d697
commit
9b6edea13b
20 changed files with 585 additions and 20 deletions
|
@ -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';
|
||||
|
|
|
@ -26,6 +26,8 @@ export type {
|
|||
DiscoverCustomization,
|
||||
DiscoverCustomizationService,
|
||||
FlyoutCustomization,
|
||||
FlyoutContentActions,
|
||||
FlyoutContentProps,
|
||||
SearchBarCustomization,
|
||||
UnifiedHistogramCustomization,
|
||||
TopNavCustomization,
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"data",
|
||||
"dataViews",
|
||||
"discover",
|
||||
"fieldFormats",
|
||||
"fleet",
|
||||
"kibanaReact",
|
||||
"kibanaUtils",
|
||||
|
|
|
@ -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<FlyoutProps, 'dataView' | 'doc' | 'actions'>) {
|
||||
const parsedDoc = useDocDetail(doc as LogDocument, { dataView });
|
||||
|
||||
const { hasTimestamp, hasLogLevel, hasMessage, hasBadges, hasFlyoutHeader } =
|
||||
getDocDetailRenderFlags(parsedDoc);
|
||||
|
||||
return hasFlyoutHeader ? (
|
||||
<EuiFlexGroup direction="column" gutterSize="m" data-test-subj="logExplorerFlyoutDetail">
|
||||
<EuiFlexItem grow={false}>
|
||||
{hasBadges && (
|
||||
<EuiFlexGroup responsive={false} gutterSize="m">
|
||||
{hasLogLevel && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<LogLevel level={parsedDoc['log.level']} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{hasTimestamp && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<Timestamp timestamp={parsedDoc['@timestamp']} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{hasMessage && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<Message message={parsedDoc.message} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
) : null;
|
||||
}
|
|
@ -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';
|
|
@ -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<string, EuiBadgeProps['color']> = {
|
||||
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 (
|
||||
<EuiBadge color={levelColor} data-test-subj="logExplorerFlyoutLogLevel">
|
||||
{level}
|
||||
</EuiBadge>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs" data-test-subj="logExplorerFlyoutLogMessage">
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="xs">
|
||||
{flyoutMessageLabel}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiCodeBlock overflowHeight={100} paddingSize="m" isCopyable language="txt" fontSize="m">
|
||||
{message}
|
||||
</EuiCodeBlock>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<EuiBadge color="hollow" data-test-subj="logExplorerFlyoutLogTimestamp">
|
||||
{timestamp}
|
||||
</EuiBadge>
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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<FlyoutProps, 'dataView'>
|
||||
): FlyoutDoc {
|
||||
const { services } = useKibanaContextForPlugin();
|
||||
|
||||
const formatField = <F extends keyof LogDocument['flattened']>(
|
||||
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,
|
||||
};
|
||||
};
|
|
@ -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 (
|
||||
<EuiFlexGroup direction="column">
|
||||
{/* Apply custom Log Explorer detail */}
|
||||
<EuiFlexItem>
|
||||
<FlyoutDetail actions={actions} dataView={dataView} doc={doc} />
|
||||
</EuiFlexItem>
|
||||
{/* Restore default content */}
|
||||
<EuiFlexItem>{renderDefaultContent()}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default CustomFlyoutContent;
|
|
@ -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 (
|
||||
<KibanaContextProviderForPlugin>
|
||||
<LazyCustomFlyoutContent {...props} dataView={internalState.dataView} />
|
||||
</KibanaContextProviderForPlugin>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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/**/*"]
|
||||
}
|
||||
|
|
|
@ -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<void>;
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<string, string>;
|
||||
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');
|
||||
},
|
||||
|
||||
async getQueryBarValue() {
|
||||
const queryBar = await testSubjects.find('queryInput');
|
||||
return queryBar.getAttribute('value');
|
||||
},
|
||||
|
||||
async typeInQueryBar(query: string) {
|
||||
const queryBar = await this.getQueryBar();
|
||||
await queryBar.clearValueWithKeyboard();
|
||||
return queryBar.type(query);
|
||||
getQueryBarValue() {
|
||||
return queryBar.getQueryString();
|
||||
},
|
||||
|
||||
async submitQuery(query: string) {
|
||||
await this.typeInQueryBar(query);
|
||||
await testSubjects.click('querySubmitButton');
|
||||
await queryBar.setQuery(query);
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface MockLogDoc {
|
||||
time: number;
|
||||
logFilepath: string;
|
||||
serviceName?: string;
|
||||
namespace: string;
|
||||
datasetName: string;
|
||||
message?: string;
|
||||
logLevel?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<void>;
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue