mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Logs Explorer] Add actions column (#175872)
## Summary
Closes - https://github.com/elastic/kibana/issues/171728
## What this PR does ?
1. Add customisation point for handling Leading and Trailing columns via
a single customisation point.
Currently the `Data Grid` exposes 2 separate customisation point for
handling this -
- `externalControlColumns` to add extension to `LeadingControlColumn`
- `trailingControlColumns` to add extension to `TrailingControlColumns`
But both of these extension point creates certain problems(Discover team
can help in here) which this new extension point solves.
1. There exists a React Bug (may be because the way we bundle Kibana,
not 100% sure) due to which when customControlColumns when passed to the
above props like `externalControlColumns` or `trailingControlColumns`,
even though they render inside the `<UnifiedDataTableContext.Provider
value={unifiedDataTableContextValue}>` provider, they actually cannot
access the context. For example when i tried to import the
`ExpandButton` from the table directly and pass as it is to
`trailingControlColumn` somehow 2 different context were getting created
and the ExpandButton present in the LeadingColumn was getting access to
context with data in it, where as the ExpandButton present in the
Trailing Column which was passed via prop was ending up in different
context and hence not receiving data.
2. Access to the `rows` data is very important. If context cannot be
accessed, then accessing the row data becomes trickier.
Hence this new Customisation Point solves the above problem by passing
in both the control columns with reference and data and the consumer can
then modify it accordingly.
2. Using the Customisation point described in point 1, 3 additional
columns were added to the newly added Marker Control column
- Expand Action. (Moved from 1st to last)
- Malformed Document (In future this will be linked to a Fix IT flow)
- Stacktraces available (In future this will be linked to the Flyout
Tab, 4th Tab which will be Stacktrace)
## Demo
<img width="2178" alt="image"
src="781eaaa3
-d354-43ff-a570-aeee4dc6e80c">
## What's pending
- [x] Add E2E tests for the Control Column new icons
- [x] Check for Memoisation probability in customControlColumns
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
5d833360f6
commit
fd8a9f07fd
41 changed files with 1119 additions and 95 deletions
|
@ -36,6 +36,9 @@ export type LogDocument = Fields &
|
|||
'cloud.availability_zone'?: string;
|
||||
'cloud.project.id'?: string;
|
||||
'cloud.instance.id'?: string;
|
||||
'error.stack_trace'?: string;
|
||||
'error.exception.stacktrace'?: string;
|
||||
'error.log.stacktrace'?: string;
|
||||
}>;
|
||||
|
||||
class Log extends Serializable<LogDocument> {
|
||||
|
|
|
@ -9,6 +9,9 @@ import { LogDocument, log, generateShortId, generateLongId } from '@kbn/apm-synt
|
|||
import { Scenario } from '../cli/scenario';
|
||||
import { withClient } from '../lib/utils/with_client';
|
||||
|
||||
const MORE_THAN_1024_CHARS =
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?';
|
||||
|
||||
const scenario: Scenario<LogDocument> = async (runOptions) => {
|
||||
return {
|
||||
generate: ({ range, clients: { logsEsClient } }) => {
|
||||
|
@ -111,6 +114,7 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
|
|||
.defaults({
|
||||
'trace.id': generateShortId(),
|
||||
'error.message': MESSAGE_LOG_LEVELS[index].message,
|
||||
'error.exception.stacktrace': 'Error message in error.exception.stacktrace',
|
||||
'agent.name': 'nodejs',
|
||||
'orchestrator.cluster.name': CLUSTER[index].clusterName,
|
||||
'orchestrator.cluster.id': CLUSTER[index].clusterId,
|
||||
|
@ -143,6 +147,7 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
|
|||
.defaults({
|
||||
'trace.id': generateShortId(),
|
||||
'event.original': MESSAGE_LOG_LEVELS[index].message,
|
||||
'error.log.stacktrace': 'Error message in error.log.stacktrace',
|
||||
'agent.name': 'nodejs',
|
||||
'orchestrator.cluster.name': CLUSTER[index].clusterName,
|
||||
'orchestrator.cluster.id': CLUSTER[index].clusterId,
|
||||
|
@ -186,6 +191,39 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
|
|||
'cloud.project.id': generateShortId(),
|
||||
'cloud.instance.id': generateShortId(),
|
||||
'log.file.path': `/logs/${generateLongId()}/error.txt`,
|
||||
'error.stack_trace': 'Error message in error.stack_trace',
|
||||
})
|
||||
.timestamp(timestamp);
|
||||
});
|
||||
});
|
||||
|
||||
const malformedDocs = range
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return Array(3)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
const index = Math.floor(Math.random() * 3);
|
||||
return log
|
||||
.create()
|
||||
.message(MESSAGE_LOG_LEVELS[index].message)
|
||||
.logLevel(MORE_THAN_1024_CHARS)
|
||||
.service(SERVICE_NAMES[index])
|
||||
.defaults({
|
||||
'trace.id': generateShortId(),
|
||||
'agent.name': 'nodejs',
|
||||
'orchestrator.cluster.name': CLUSTER[index].clusterName,
|
||||
'orchestrator.cluster.id': CLUSTER[index].clusterId,
|
||||
'orchestrator.namespace': CLUSTER[index].namespace,
|
||||
'container.name': `${SERVICE_NAMES[index]}-${generateShortId()}`,
|
||||
'orchestrator.resource.id': generateShortId(),
|
||||
'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)],
|
||||
'cloud.region': CLOUD_REGION[index],
|
||||
'cloud.availability_zone': MORE_THAN_1024_CHARS,
|
||||
'cloud.project.id': generateShortId(),
|
||||
'cloud.instance.id': generateShortId(),
|
||||
'log.file.path': `/logs/${generateLongId()}/error.txt`,
|
||||
})
|
||||
.timestamp(timestamp);
|
||||
});
|
||||
|
@ -199,6 +237,7 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
|
|||
logsWithErrorMessage,
|
||||
logsWithEventMessage,
|
||||
logsWithNoMessage,
|
||||
malformedDocs,
|
||||
])
|
||||
);
|
||||
},
|
||||
|
|
|
@ -90,7 +90,7 @@ pageLoadAssetSize:
|
|||
licensing: 29004
|
||||
links: 44490
|
||||
lists: 22900
|
||||
logsExplorer: 50000
|
||||
logsExplorer: 55000
|
||||
logsShared: 281060
|
||||
logstash: 53548
|
||||
management: 46112
|
||||
|
|
|
@ -25,3 +25,5 @@ export { getRowsPerPageOptions } from './src/utils/rows_per_page';
|
|||
export { popularizeField } from './src/utils/popularize_field';
|
||||
|
||||
export { useColumns } from './src/hooks/use_data_grid_columns';
|
||||
export { OPEN_DETAILS, SELECT_ROW } from './src/components/data_table_columns';
|
||||
export { DataTableRowControl } from './src/components/data_table_row_control';
|
||||
|
|
|
@ -24,7 +24,10 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
|||
import { servicesMock } from '../../__mocks__/services';
|
||||
import { buildDataTableRecord, getDocId } from '@kbn/discover-utils';
|
||||
import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types';
|
||||
import { testLeadingControlColumn } from '../../__mocks__/external_control_columns';
|
||||
import {
|
||||
testLeadingControlColumn,
|
||||
testTrailingControlColumns,
|
||||
} from '../../__mocks__/external_control_columns';
|
||||
|
||||
const mockUseDataGridColumnsCellActions = jest.fn((prop: unknown) => []);
|
||||
jest.mock('@kbn/cell-actions', () => ({
|
||||
|
@ -418,6 +421,69 @@ describe('UnifiedDataTable', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('customControlColumnsConfiguration', () => {
|
||||
const customControlColumnsConfiguration = jest.fn();
|
||||
it('should be able to customise the leading control column', async () => {
|
||||
const component = await getComponent({
|
||||
...getProps(),
|
||||
expandedDoc: {
|
||||
id: 'test',
|
||||
raw: {
|
||||
_index: 'test_i',
|
||||
_id: 'test',
|
||||
},
|
||||
flattened: { test: jest.fn() },
|
||||
},
|
||||
setExpandedDoc: jest.fn(),
|
||||
renderDocumentView: jest.fn(),
|
||||
externalControlColumns: [testLeadingControlColumn],
|
||||
customControlColumnsConfiguration: customControlColumnsConfiguration.mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
leadingControlColumns: [testLeadingControlColumn, testTrailingControlColumns[0]],
|
||||
trailingControlColumns: [],
|
||||
};
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
expect(findTestSubject(component, 'test-body-control-column-cell').exists()).toBeTruthy();
|
||||
expect(
|
||||
findTestSubject(component, 'test-trailing-column-popover-button').exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be able to customise the trailing control column', async () => {
|
||||
const component = await getComponent({
|
||||
...getProps(),
|
||||
expandedDoc: {
|
||||
id: 'test',
|
||||
raw: {
|
||||
_index: 'test_i',
|
||||
_id: 'test',
|
||||
},
|
||||
flattened: { test: jest.fn() },
|
||||
},
|
||||
setExpandedDoc: jest.fn(),
|
||||
renderDocumentView: jest.fn(),
|
||||
externalControlColumns: [testLeadingControlColumn],
|
||||
customControlColumnsConfiguration: customControlColumnsConfiguration.mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
leadingControlColumns: [],
|
||||
trailingControlColumns: [testLeadingControlColumn, testTrailingControlColumns[0]],
|
||||
};
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
expect(findTestSubject(component, 'test-body-control-column-cell').exists()).toBeTruthy();
|
||||
expect(
|
||||
findTestSubject(component, 'test-trailing-column-popover-button').exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('externalControlColumns', () => {
|
||||
it('should render external leading control columns', async () => {
|
||||
const component = await getComponent({
|
||||
|
|
|
@ -52,12 +52,14 @@ import {
|
|||
DataTableColumnTypes,
|
||||
CustomCellRenderer,
|
||||
CustomGridColumnsConfiguration,
|
||||
CustomControlColumnConfiguration,
|
||||
} from '../types';
|
||||
import { getDisplayedColumns } from '../utils/columns';
|
||||
import { convertValueToString } from '../utils/convert_value_to_string';
|
||||
import { getRowsPerPageOptions } from '../utils/rows_per_page';
|
||||
import { getRenderCellValueFn } from '../utils/get_render_cell_value';
|
||||
import {
|
||||
getAllControlColumns,
|
||||
getEuiGridColumns,
|
||||
getLeadControlColumns,
|
||||
getVisibleColumns,
|
||||
|
@ -334,6 +336,10 @@ export interface UnifiedDataTableProps {
|
|||
* An optional settings for customising the column
|
||||
*/
|
||||
customGridColumnsConfiguration?: CustomGridColumnsConfiguration;
|
||||
/**
|
||||
* An optional settings to control which columns to render as trailing and leading control columns
|
||||
*/
|
||||
customControlColumnsConfiguration?: CustomControlColumnConfiguration;
|
||||
/**
|
||||
* Name of the UnifiedDataTable consumer component or application
|
||||
*/
|
||||
|
@ -412,6 +418,7 @@ export const UnifiedDataTable = ({
|
|||
gridStyleOverride,
|
||||
rowLineHeightOverride,
|
||||
customGridColumnsConfiguration,
|
||||
customControlColumnsConfiguration,
|
||||
}: UnifiedDataTableProps) => {
|
||||
const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings, storage, data } =
|
||||
services;
|
||||
|
@ -746,19 +753,31 @@ export const UnifiedDataTable = ({
|
|||
onSort: onTableSort,
|
||||
};
|
||||
}
|
||||
return { columns: sortingColumns, onSort: () => {} };
|
||||
return {
|
||||
columns: sortingColumns,
|
||||
onSort: () => {},
|
||||
};
|
||||
}, [isSortEnabled, sortingColumns, isPlainRecord, inmemorySortingColumns, onTableSort]);
|
||||
|
||||
const canSetExpandedDoc = Boolean(setExpandedDoc && !!renderDocumentView);
|
||||
|
||||
const leadingControlColumns = useMemo(() => {
|
||||
const leadingControlColumns: EuiDataGridControlColumn[] = useMemo(() => {
|
||||
const internalControlColumns = getLeadControlColumns(canSetExpandedDoc).filter(({ id }) =>
|
||||
controlColumnIds.includes(id)
|
||||
);
|
||||
return externalControlColumns
|
||||
? [...internalControlColumns, ...externalControlColumns]
|
||||
: internalControlColumns;
|
||||
}, [canSetExpandedDoc, externalControlColumns, controlColumnIds]);
|
||||
}, [canSetExpandedDoc, controlColumnIds, externalControlColumns]);
|
||||
|
||||
const controlColumnsConfig = customControlColumnsConfiguration?.({
|
||||
controlColumns: getAllControlColumns(),
|
||||
});
|
||||
|
||||
const customLeadingControlColumn =
|
||||
controlColumnsConfig?.leadingControlColumns ?? leadingControlColumns;
|
||||
const customTrailingControlColumn =
|
||||
controlColumnsConfig?.trailingControlColumns ?? trailingControlColumns;
|
||||
|
||||
const additionalControls = useMemo(() => {
|
||||
if (!externalAdditionalControls && !usedSelectedDocs.length) {
|
||||
|
@ -907,7 +926,7 @@ export const UnifiedDataTable = ({
|
|||
columns={euiGridColumns}
|
||||
columnVisibility={columnsVisibility}
|
||||
data-test-subj="docTable"
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
leadingControlColumns={customLeadingControlColumn}
|
||||
onColumnResize={onResize}
|
||||
pagination={paginationObj}
|
||||
renderCellValue={renderCellValue}
|
||||
|
@ -921,7 +940,7 @@ export const UnifiedDataTable = ({
|
|||
gridStyle={gridStyleOverride ?? GRID_STYLE}
|
||||
renderCustomGridBody={renderCustomGridBody}
|
||||
renderCustomToolbar={renderCustomToolbarFn}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
trailingControlColumns={customTrailingControlColumn}
|
||||
/>
|
||||
</div>
|
||||
{loadingState !== DataLoadingState.loading &&
|
||||
|
|
|
@ -17,7 +17,7 @@ import type { DataView } from '@kbn/data-views-plugin/public';
|
|||
import { ToastsStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
import { ExpandButton } from './data_table_expand_button';
|
||||
import { CustomGridColumnsConfiguration, UnifiedDataTableSettings } from '../types';
|
||||
import { ControlColumns, CustomGridColumnsConfiguration, UnifiedDataTableSettings } from '../types';
|
||||
import type { ValueToStringConverter, DataTableColumnTypes } from '../types';
|
||||
import { buildCellActions } from './default_cell_actions';
|
||||
import { getSchemaByKbnType } from './data_table_schema';
|
||||
|
@ -30,8 +30,11 @@ import { DataTableColumnHeader, DataTableTimeColumnHeader } from './data_table_c
|
|||
const DataTableColumnHeaderMemoized = React.memo(DataTableColumnHeader);
|
||||
const DataTableTimeColumnHeaderMemoized = React.memo(DataTableTimeColumnHeader);
|
||||
|
||||
export const OPEN_DETAILS = 'openDetails';
|
||||
export const SELECT_ROW = 'select';
|
||||
|
||||
const openDetails = {
|
||||
id: 'openDetails',
|
||||
id: OPEN_DETAILS,
|
||||
width: 26,
|
||||
headerCellRender: () => (
|
||||
<EuiScreenReaderOnly>
|
||||
|
@ -46,7 +49,7 @@ const openDetails = {
|
|||
};
|
||||
|
||||
const select = {
|
||||
id: 'select',
|
||||
id: SELECT_ROW,
|
||||
width: 24,
|
||||
rowCellRender: SelectButton,
|
||||
headerCellRender: () => (
|
||||
|
@ -60,6 +63,13 @@ const select = {
|
|||
),
|
||||
};
|
||||
|
||||
export function getAllControlColumns(): ControlColumns {
|
||||
return {
|
||||
[SELECT_ROW]: select,
|
||||
[OPEN_DETAILS]: openDetails,
|
||||
};
|
||||
}
|
||||
|
||||
export function getLeadControlColumns(canSetExpandedDoc: boolean) {
|
||||
if (!canSetExpandedDoc) {
|
||||
return [select];
|
||||
|
|
|
@ -11,6 +11,7 @@ import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@el
|
|||
import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-theme';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UnifiedDataTableContext } from '../table_context';
|
||||
import { DataTableRowControl } from './data_table_row_control';
|
||||
|
||||
/**
|
||||
* Button to expand a given row
|
||||
|
@ -62,7 +63,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="unifiedDataTable__rowControl">
|
||||
<DataTableRowControl>
|
||||
<EuiToolTip content={buttonLabel} delay="long" ref={toolTipRef}>
|
||||
<EuiButtonIcon
|
||||
id={rowIndex === 0 ? tourStep : undefined}
|
||||
|
@ -81,6 +82,6 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
|
|||
isSelected={isCurrentRowExpanded}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
</DataTableRowControl>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 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';
|
||||
|
||||
export const DataTableRowControl = ({ children }: { children: React.ReactNode }) => {
|
||||
return <span className="unifiedDataTable__rowControl">{children}</span>;
|
||||
};
|
|
@ -11,6 +11,7 @@ import { EuiDataGridCellValueElementProps, type EuiDataGridColumn } from '@elast
|
|||
import type { DataTableRecord } from '@kbn/discover-utils/src/types';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { EuiDataGridControlColumn } from '@elastic/eui/src/components/datagrid/data_grid_types';
|
||||
|
||||
/**
|
||||
* User configurable state of data grid, persisted in saved search
|
||||
|
@ -55,3 +56,17 @@ export type CustomGridColumnsConfiguration = Record<
|
|||
string,
|
||||
(props: CustomGridColumnProps) => EuiDataGridColumn
|
||||
>;
|
||||
|
||||
export interface ControlColumns {
|
||||
select: EuiDataGridControlColumn;
|
||||
openDetails: EuiDataGridControlColumn;
|
||||
}
|
||||
|
||||
export interface ControlColumnsProps {
|
||||
controlColumns: ControlColumns;
|
||||
}
|
||||
|
||||
export type CustomControlColumnConfiguration = (props: ControlColumnsProps) => {
|
||||
leadingControlColumns: EuiDataGridControlColumn[];
|
||||
trailingControlColumns?: EuiDataGridControlColumn[];
|
||||
};
|
||||
|
|
|
@ -120,10 +120,16 @@ describe('Discover documents layout', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
const customControlColumnsConfiguration = () => ({
|
||||
leadingControlColumns: [],
|
||||
trailingControlColumns: [],
|
||||
});
|
||||
|
||||
const customization: DiscoverCustomization = {
|
||||
id: 'data_table',
|
||||
customCellRenderer,
|
||||
customGridColumnsConfiguration,
|
||||
customControlColumnsConfiguration,
|
||||
};
|
||||
|
||||
customisationService.set(customization);
|
||||
|
@ -135,5 +141,8 @@ describe('Discover documents layout', () => {
|
|||
expect(discoverGridComponent.prop('customGridColumnsConfiguration')).toEqual(
|
||||
customGridColumnsConfiguration
|
||||
);
|
||||
expect(discoverGridComponent.prop('customControlColumnsConfiguration')).toEqual(
|
||||
customControlColumnsConfiguration
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -255,9 +255,11 @@ function DiscoverDocumentsComponent({
|
|||
[dataView, onAddColumn, onAddFilter, onRemoveColumn, query, savedSearch.id, setExpandedDoc]
|
||||
);
|
||||
|
||||
const externalCustomRenderers = useDiscoverCustomization('data_table')?.customCellRenderer;
|
||||
const customGridColumnsConfiguration =
|
||||
useDiscoverCustomization('data_table')?.customGridColumnsConfiguration;
|
||||
const {
|
||||
customCellRenderer: externalCustomRenderers,
|
||||
customGridColumnsConfiguration,
|
||||
customControlColumnsConfiguration,
|
||||
} = useDiscoverCustomization('data_table') || {};
|
||||
|
||||
const documents = useObservable(stateContainer.dataState.data$.documents$);
|
||||
|
||||
|
@ -427,6 +429,7 @@ function DiscoverDocumentsComponent({
|
|||
headerRowHeight={3}
|
||||
externalCustomRenderers={externalCustomRenderers}
|
||||
customGridColumnsConfiguration={customGridColumnsConfiguration}
|
||||
customControlColumnsConfiguration={customControlColumnsConfiguration}
|
||||
/>
|
||||
</CellActionsProvider>
|
||||
</div>
|
||||
|
|
|
@ -6,10 +6,15 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CustomCellRenderer, CustomGridColumnsConfiguration } from '@kbn/unified-data-table';
|
||||
import {
|
||||
CustomCellRenderer,
|
||||
CustomControlColumnConfiguration,
|
||||
CustomGridColumnsConfiguration,
|
||||
} from '@kbn/unified-data-table';
|
||||
|
||||
export interface DataTableCustomization {
|
||||
id: 'data_table';
|
||||
customCellRenderer?: CustomCellRenderer;
|
||||
customGridColumnsConfiguration?: CustomGridColumnsConfiguration;
|
||||
customControlColumnsConfiguration?: CustomControlColumnConfiguration;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
|||
export type { ISearchEmbeddable, SearchInput } from './embeddable';
|
||||
export type { DiscoverAppState } from './application/main/services/discover_app_state_container';
|
||||
export type { DiscoverStateContainer } from './application/main/services/discover_state';
|
||||
export type { DataDocumentsMsg } from './application/main/services/discover_data_state_container';
|
||||
export type { DiscoverContainerProps } from './components/discover_container';
|
||||
export type {
|
||||
CustomizationCallback,
|
||||
|
|
|
@ -15,9 +15,11 @@ export interface TabbedGridData {
|
|||
columns: string[];
|
||||
rows: string[][];
|
||||
}
|
||||
|
||||
interface SelectOptions {
|
||||
isAnchorRow?: boolean;
|
||||
rowIndex?: number;
|
||||
columnIndex?: number;
|
||||
renderMoreRows?: boolean;
|
||||
}
|
||||
|
||||
|
@ -242,13 +244,14 @@ export class DataGridService extends FtrService {
|
|||
}
|
||||
|
||||
public async clickRowToggle(
|
||||
options: SelectOptions = { isAnchorRow: false, rowIndex: 0 }
|
||||
options: SelectOptions = { isAnchorRow: false, rowIndex: 0, columnIndex: 0 }
|
||||
): Promise<void> {
|
||||
const row = await this.getRow(options);
|
||||
const rowColumns = await this.getRow(options);
|
||||
const testSubj = options.isAnchorRow
|
||||
? '~docTableExpandToggleColumnAnchor'
|
||||
: '~docTableExpandToggleColumn';
|
||||
const toggle = await row[0].findByTestSubject(testSubj);
|
||||
|
||||
const toggle = await rowColumns[options.columnIndex ?? 0].findByTestSubject(testSubj);
|
||||
|
||||
await toggle.scrollIntoViewIfNecessary();
|
||||
await toggle.click();
|
||||
|
@ -272,7 +275,20 @@ export class DataGridService extends FtrService {
|
|||
const cellText = await cell.getVisibleText();
|
||||
textArr.push(cellText.trim());
|
||||
}
|
||||
return Promise.resolve(textArr);
|
||||
return textArr;
|
||||
}
|
||||
|
||||
public async getControlColumnHeaderFields(): Promise<string[]> {
|
||||
const result = await this.find.allByCssSelector(
|
||||
'.euiDataGridHeaderCell--controlColumn > .euiDataGridHeaderCell__content'
|
||||
);
|
||||
|
||||
const textArr = [];
|
||||
for (const cell of result) {
|
||||
const cellText = await cell.getVisibleText();
|
||||
textArr.push(cellText.trim());
|
||||
}
|
||||
return textArr;
|
||||
}
|
||||
|
||||
public async getRowActions(
|
||||
|
@ -393,6 +409,7 @@ export class DataGridService extends FtrService {
|
|||
const detailRows = await this.getDetailsRows();
|
||||
return detailRows[0];
|
||||
}
|
||||
|
||||
public async addInclusiveFilter(detailsRow: WebElementWrapper, fieldName: string): Promise<void> {
|
||||
const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName);
|
||||
const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow);
|
||||
|
|
|
@ -34,6 +34,14 @@ export const ORCHESTRATOR_NAMESPACE_FIELD = 'orchestrator.namespace';
|
|||
export const CONTAINER_NAME_FIELD = 'container.name';
|
||||
export const CONTAINER_ID_FIELD = 'container.id';
|
||||
|
||||
// Malformed Docs
|
||||
export const MALFORMED_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';
|
||||
|
||||
// Virtual column fields
|
||||
export const CONTENT_FIELD = 'content';
|
||||
export const RESOURCE_FIELD = 'resource';
|
||||
|
@ -41,7 +49,7 @@ export const RESOURCE_FIELD = 'resource';
|
|||
// Sizing
|
||||
export const DATA_GRID_COLUMN_WIDTH_SMALL = 240;
|
||||
export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320;
|
||||
|
||||
export const ACTIONS_COLUMN_WIDTH = 80;
|
||||
// UI preferences
|
||||
export const DEFAULT_COLUMNS = [
|
||||
{
|
||||
|
|
|
@ -33,6 +33,10 @@ export interface LogDocument extends DataTableRecord {
|
|||
'log.file.path'?: string;
|
||||
'data_stream.namespace': string;
|
||||
'data_stream.dataset': string;
|
||||
|
||||
'error.stack_trace'?: string;
|
||||
'error.exception.stacktrace'?: string;
|
||||
'error.log.stacktrace'?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -74,3 +78,9 @@ export interface ResourceFields {
|
|||
'container.id'?: string;
|
||||
'cloud.instance.id'?: string;
|
||||
}
|
||||
|
||||
export interface StackTraceFields {
|
||||
'error.stack_trace'?: string;
|
||||
'error.exception.stacktrace'?: string;
|
||||
'error.log.stacktrace'?: string;
|
||||
}
|
||||
|
|
|
@ -31,11 +31,13 @@ export function LogLevel({ level, dataTestSubj, renderInFlyout = false }: LogLev
|
|||
? euiTheme.colors[LEVEL_DICT[level as keyof typeof LEVEL_DICT]]
|
||||
: null;
|
||||
|
||||
const truncatedLogLevel = level.length > 10 ? level.substring(0, 10) + '...' : level;
|
||||
|
||||
if (renderInFlyout) {
|
||||
return (
|
||||
<ChipWithPopover
|
||||
property={constants.LOG_LEVEL_FIELD}
|
||||
text={level}
|
||||
text={truncatedLogLevel}
|
||||
borderColor={levelColor}
|
||||
style={{ width: 'none' }}
|
||||
dataTestSubj={dataTestSubj}
|
||||
|
@ -50,7 +52,7 @@ export function LogLevel({ level, dataTestSubj, renderInFlyout = false }: LogLev
|
|||
text={level}
|
||||
rightSideIcon="arrowDown"
|
||||
borderColor={levelColor}
|
||||
style={{ width: '80px' }}
|
||||
style={{ width: '80px', marginTop: '-3px' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ export function ChipWithPopover({
|
|||
font-size: ${xsFontSize};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: -3px;
|
||||
cursor: pointer;
|
||||
`}
|
||||
style={style}
|
||||
>
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiCode } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
export const flyoutContentLabel = i18n.translate('xpack.logsExplorer.flyoutDetail.label.message', {
|
||||
defaultMessage: 'Content breakdown',
|
||||
|
@ -22,6 +25,17 @@ export const resourceLabel = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const actionsLabel = i18n.translate('xpack.logsExplorer.dataTable.header.popover.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
});
|
||||
|
||||
export const actionsLabelLowerCase = i18n.translate(
|
||||
'xpack.logsExplorer.dataTable.header.popover.actions.lowercase',
|
||||
{
|
||||
defaultMessage: 'actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const flyoutServiceLabel = i18n.translate('xpack.logsExplorer.flyoutDetail.label.service', {
|
||||
defaultMessage: 'Service',
|
||||
});
|
||||
|
@ -208,17 +222,21 @@ export const closeCellActionPopoverText = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const contentHeaderTooltipParagraph1 = i18n.translate(
|
||||
'xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1',
|
||||
{
|
||||
defaultMessage: "Fields that provide information on the document's source, such as:",
|
||||
}
|
||||
export const contentHeaderTooltipParagraph1 = (
|
||||
<FormattedMessage
|
||||
id="xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1"
|
||||
defaultMessage="Displays the document's {logLevel} and {message} fields."
|
||||
values={{
|
||||
logLevel: <strong>log.level</strong>,
|
||||
message: <strong>message</strong>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const contentHeaderTooltipParagraph2 = i18n.translate(
|
||||
'xpack.logsExplorer.dataTable.header.content.tooltip.paragraph2',
|
||||
{
|
||||
defaultMessage: 'When the message field is empty, one of the following is displayed',
|
||||
defaultMessage: 'When the message field is empty, one of the following is displayed:',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -228,3 +246,63 @@ export const resourceHeaderTooltipParagraph = i18n.translate(
|
|||
defaultMessage: "Fields that provide information on the document's source, such as:",
|
||||
}
|
||||
);
|
||||
|
||||
export const actionsHeaderTooltipParagraph = i18n.translate(
|
||||
'xpack.logsExplorer.dataTable.header.actions.tooltip.paragraph',
|
||||
{
|
||||
defaultMessage: 'Fields that provide actionable information, such as:',
|
||||
}
|
||||
);
|
||||
|
||||
export const actionsHeaderTooltipExpandAction = i18n.translate(
|
||||
'xpack.logsExplorer.dataTable.header.actions.tooltip.expand',
|
||||
{ defaultMessage: 'Expand log details' }
|
||||
);
|
||||
|
||||
export const actionsHeaderTooltipMalformedAction = (
|
||||
<FormattedMessage
|
||||
id="xpack.logsExplorer.dataTable.controlColumn.actions.button.malformedDoc"
|
||||
defaultMessage="Access to malformed doc with {ignoredProperty} field"
|
||||
values={{
|
||||
ignoredProperty: (
|
||||
<EuiCode language="json" transparentBackground>
|
||||
_ignored
|
||||
</EuiCode>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const actionsHeaderTooltipStacktraceAction = i18n.translate(
|
||||
'xpack.logsExplorer.dataTable.header.actions.tooltip.stacktrace',
|
||||
{ defaultMessage: 'Access to available stacktraces based on:' }
|
||||
);
|
||||
|
||||
export const malformedDocButtonLabelWhenPresent = i18n.translate(
|
||||
'xpack.logsExplorer.dataTable.controlColumn.actions.button.malformedDocPresent',
|
||||
{
|
||||
defaultMessage:
|
||||
"This document couldn't be parsed correctly. Not all fields are properly populated",
|
||||
}
|
||||
);
|
||||
|
||||
export const malformedDocButtonLabelWhenNotPresent = i18n.translate(
|
||||
'xpack.logsExplorer.dataTable.controlColumn.actions.button.malformedDocNotPresent',
|
||||
{
|
||||
defaultMessage: 'All fields in this document were parsed correctly',
|
||||
}
|
||||
);
|
||||
|
||||
export const stacktraceAvailableControlButton = i18n.translate(
|
||||
'xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.available',
|
||||
{
|
||||
defaultMessage: 'Stacktraces available',
|
||||
}
|
||||
);
|
||||
|
||||
export const stacktraceNotAvailableControlButton = i18n.translate(
|
||||
'xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.notAvailable',
|
||||
{
|
||||
defaultMessage: 'Stacktraces not available',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import {
|
||||
actionsHeaderTooltipExpandAction,
|
||||
actionsHeaderTooltipMalformedAction,
|
||||
actionsHeaderTooltipParagraph,
|
||||
actionsHeaderTooltipStacktraceAction,
|
||||
actionsLabel,
|
||||
actionsLabelLowerCase,
|
||||
} from '../../common/translations';
|
||||
import { HoverPopover } from '../../common/hover_popover';
|
||||
import { TooltipButtonComponent } from './tooltip_button';
|
||||
import * as constants from '../../../../common/constants';
|
||||
import { FieldWithToken } from './field_with_token';
|
||||
|
||||
const spacingCSS = css`
|
||||
margin-bottom: ${euiThemeVars.euiSizeS};
|
||||
`;
|
||||
|
||||
export const ActionsColumnTooltip = () => {
|
||||
return (
|
||||
<HoverPopover
|
||||
button={<TooltipButtonComponent displayText={actionsLabelLowerCase} />}
|
||||
title={actionsLabel}
|
||||
>
|
||||
<div style={{ width: '230px' }}>
|
||||
<EuiText size="s" css={spacingCSS}>
|
||||
<p>{actionsHeaderTooltipParagraph}</p>
|
||||
</EuiText>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
alignItems="baseline"
|
||||
justifyContent="flexStart"
|
||||
gutterSize="s"
|
||||
css={spacingCSS}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="expand" size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
<p>{actionsHeaderTooltipExpandAction}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
alignItems="baseline"
|
||||
justifyContent="flexStart"
|
||||
gutterSize="s"
|
||||
css={spacingCSS}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="indexClose" size="s" color="danger" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
<p>{actionsHeaderTooltipMalformedAction}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
alignItems="baseline"
|
||||
justifyContent="flexStart"
|
||||
gutterSize="s"
|
||||
css={spacingCSS}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="apmTrace" size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
<p>{actionsHeaderTooltipStacktraceAction}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<div style={{ marginLeft: '15px' }}>
|
||||
{[
|
||||
constants.ERROR_STACK_TRACE,
|
||||
constants.ERROR_EXCEPTION_STACKTRACE,
|
||||
constants.ERROR_LOG_STACKTRACE,
|
||||
].map((field) => (
|
||||
<FieldWithToken field={field} key={field} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</HoverPopover>
|
||||
);
|
||||
};
|
|
@ -42,7 +42,7 @@ export const ResourceColumnTooltip = ({ column, headerRowHeight }: CustomGridCol
|
|||
constants.HOST_NAME_FIELD,
|
||||
constants.CLOUD_INSTANCE_ID_FIELD,
|
||||
].map((field) => (
|
||||
<FieldWithToken field={field} />
|
||||
<FieldWithToken field={field} key={field} />
|
||||
))}
|
||||
</div>
|
||||
</HoverPopover>
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ComponentClass } from 'react';
|
||||
import {
|
||||
OPEN_DETAILS,
|
||||
SELECT_ROW,
|
||||
type ControlColumnsProps,
|
||||
DataTableRowControl,
|
||||
} from '@kbn/unified-data-table';
|
||||
import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/src/types';
|
||||
import { useActor } from '@xstate/react';
|
||||
import { LogsExplorerControllerStateService } from '../state_machines/logs_explorer_controller';
|
||||
import {
|
||||
malformedDocButtonLabelWhenNotPresent,
|
||||
malformedDocButtonLabelWhenPresent,
|
||||
stacktraceAvailableControlButton,
|
||||
stacktraceNotAvailableControlButton,
|
||||
} from '../components/common/translations';
|
||||
import * as constants from '../../common/constants';
|
||||
import { getStacktraceFields } from '../utils/get_stack_trace';
|
||||
import { LogDocument } from '../../common/document';
|
||||
import { ActionsColumnTooltip } from '../components/virtual_columns/column_tooltips/actions_column_tooltip';
|
||||
|
||||
const ConnectedMalformedDocs = ({
|
||||
rowIndex,
|
||||
service,
|
||||
}: {
|
||||
rowIndex: number;
|
||||
service: LogsExplorerControllerStateService;
|
||||
}) => {
|
||||
const [state] = useActor(service);
|
||||
|
||||
if (state.matches('initialized') && state.context.rows) {
|
||||
const row = state.context.rows[rowIndex];
|
||||
return <MalformedDocs row={row} rowIndex={rowIndex} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ConnectedStacktraceDocs = ({
|
||||
rowIndex,
|
||||
service,
|
||||
}: {
|
||||
rowIndex: number;
|
||||
service: LogsExplorerControllerStateService;
|
||||
}) => {
|
||||
const [state] = useActor(service);
|
||||
|
||||
if (state.matches('initialized') && state.context.rows) {
|
||||
const row = state.context.rows[rowIndex];
|
||||
return <Stacktrace row={row} rowIndex={rowIndex} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const MalformedDocs = ({ row, rowIndex }: { row: DataTableRecord; rowIndex: number }) => {
|
||||
const isMalformedDocumentExists = !!row.raw[constants.MALFORMED_DOCS_FIELD];
|
||||
|
||||
return isMalformedDocumentExists ? (
|
||||
<DataTableRowControl>
|
||||
<EuiToolTip content={malformedDocButtonLabelWhenPresent} delay="long">
|
||||
<EuiButtonIcon
|
||||
id={`malformedDocExists_${rowIndex}`}
|
||||
size="xs"
|
||||
iconSize="s"
|
||||
data-test-subj={'docTableMalformedDocExist'}
|
||||
color={'danger'}
|
||||
aria-label={malformedDocButtonLabelWhenPresent}
|
||||
iconType={'indexClose'}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</DataTableRowControl>
|
||||
) : (
|
||||
<DataTableRowControl>
|
||||
<EuiToolTip content={malformedDocButtonLabelWhenNotPresent} delay="long">
|
||||
<EuiButtonIcon
|
||||
id={`malformedDocExists_${rowIndex}`}
|
||||
size="xs"
|
||||
iconSize="s"
|
||||
data-test-subj={'docTableMalformedDocDoesNotExist'}
|
||||
color={'text'}
|
||||
iconType={'pagesSelect'}
|
||||
aria-label={malformedDocButtonLabelWhenNotPresent}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</DataTableRowControl>
|
||||
);
|
||||
};
|
||||
|
||||
const Stacktrace = ({ row, rowIndex }: { row: DataTableRecord; rowIndex: number }) => {
|
||||
const stacktrace = getStacktraceFields(row as LogDocument);
|
||||
const hasValue = Object.values(stacktrace).some((value) => value);
|
||||
|
||||
return (
|
||||
<DataTableRowControl>
|
||||
<EuiToolTip
|
||||
content={hasValue ? stacktraceAvailableControlButton : stacktraceNotAvailableControlButton}
|
||||
delay="long"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
id={`stacktrace_${rowIndex}`}
|
||||
size="xs"
|
||||
iconSize="s"
|
||||
data-test-subj={hasValue ? 'docTableStacktraceExist' : 'docTableStacktraceDoesNotExist'}
|
||||
color={'text'}
|
||||
iconType={'apmTrace'}
|
||||
aria-label={
|
||||
hasValue ? stacktraceAvailableControlButton : stacktraceNotAvailableControlButton
|
||||
}
|
||||
disabled={!hasValue}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</DataTableRowControl>
|
||||
);
|
||||
};
|
||||
|
||||
export const createCustomControlColumnsConfiguration =
|
||||
(service: LogsExplorerControllerStateService) =>
|
||||
({ controlColumns }: ControlColumnsProps) => {
|
||||
const checkBoxColumn = controlColumns[SELECT_ROW];
|
||||
const openDetails = controlColumns[OPEN_DETAILS];
|
||||
const ExpandButton =
|
||||
openDetails.rowCellRender as ComponentClass<EuiDataGridCellValueElementProps>;
|
||||
const actionsColumn = {
|
||||
id: 'actionsColumn',
|
||||
width: constants.ACTIONS_COLUMN_WIDTH,
|
||||
headerCellRender: ActionsColumnTooltip,
|
||||
rowCellRender: ({ rowIndex, setCellProps, ...rest }: EuiDataGridCellValueElementProps) => {
|
||||
return (
|
||||
<span>
|
||||
<ExpandButton rowIndex={rowIndex} setCellProps={setCellProps} {...rest} />
|
||||
<ConnectedMalformedDocs rowIndex={rowIndex} service={service} />
|
||||
<ConnectedStacktraceDocs rowIndex={rowIndex} service={service} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
leadingControlColumns: [checkBoxColumn],
|
||||
trailingControlColumns: [actionsColumn],
|
||||
};
|
||||
};
|
|
@ -87,6 +87,9 @@ export const createLogsExplorerProfileCustomizations =
|
|||
id: 'data_table',
|
||||
customCellRenderer: createCustomCellRenderer({ data }),
|
||||
customGridColumnsConfiguration: createCustomGridColumnsConfiguration(),
|
||||
customControlColumnsConfiguration: await import('./custom_control_column').then((module) =>
|
||||
module.createCustomControlColumnsConfiguration(service)
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -40,4 +40,5 @@ export const DEFAULT_CONTEXT: DefaultLogsExplorerControllerState = {
|
|||
from: 'now-15m/m',
|
||||
to: 'now',
|
||||
},
|
||||
rows: [],
|
||||
};
|
||||
|
|
|
@ -26,9 +26,9 @@ export const subscribeToDiscoverState =
|
|||
throw new Error('Failed to subscribe to the Discover state: no state container in context.');
|
||||
}
|
||||
|
||||
const { appState } = context.discoverStateContainer;
|
||||
const { appState, dataState } = context.discoverStateContainer;
|
||||
|
||||
const subscription = appState.state$.subscribe({
|
||||
const appStateSubscription = appState.state$.subscribe({
|
||||
next: (newAppState) => {
|
||||
if (isEmpty(newAppState)) {
|
||||
return;
|
||||
|
@ -41,8 +41,20 @@ export const subscribeToDiscoverState =
|
|||
},
|
||||
});
|
||||
|
||||
const dataStateSubscription = dataState.data$.documents$.subscribe({
|
||||
next: (newDataState) => {
|
||||
if (!isEmpty(newDataState?.result)) {
|
||||
send({
|
||||
type: 'RECEIVE_DISCOVER_DATA_STATE',
|
||||
dataState: newDataState.result,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
appStateSubscription.unsubscribe();
|
||||
dataStateSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -71,6 +83,19 @@ export const updateContextFromDiscoverAppState = actions.assign<
|
|||
return {};
|
||||
});
|
||||
|
||||
export const updateContextFromDiscoverDataState = actions.assign<
|
||||
LogsExplorerControllerContext,
|
||||
LogsExplorerControllerEvent
|
||||
>((context, event) => {
|
||||
if ('dataState' in event && event.type === 'RECEIVE_DISCOVER_DATA_STATE') {
|
||||
return {
|
||||
rows: event.dataState,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
export const updateDiscoverAppStateFromContext: ActionFunction<
|
||||
LogsExplorerControllerContext,
|
||||
LogsExplorerControllerEvent
|
||||
|
|
|
@ -25,6 +25,7 @@ import { createAndSetDataView } from './services/data_view_service';
|
|||
import {
|
||||
subscribeToDiscoverState,
|
||||
updateContextFromDiscoverAppState,
|
||||
updateContextFromDiscoverDataState,
|
||||
updateDiscoverAppStateFromContext,
|
||||
} from './services/discover_service';
|
||||
import { validateSelection } from './services/selection_service';
|
||||
|
@ -103,6 +104,7 @@ export const createPureLogsExplorerControllerStateMachine = (
|
|||
id: 'timefilterService',
|
||||
},
|
||||
],
|
||||
entry: ['resetRows'],
|
||||
states: {
|
||||
datasetSelection: {
|
||||
initial: 'validatingSelection',
|
||||
|
@ -196,6 +198,9 @@ export const createPureLogsExplorerControllerStateMachine = (
|
|||
RECEIVE_DISCOVER_APP_STATE: {
|
||||
actions: ['updateContextFromDiscoverAppState'],
|
||||
},
|
||||
RECEIVE_DISCOVER_DATA_STATE: {
|
||||
actions: ['updateContextFromDiscoverDataState'],
|
||||
},
|
||||
RECEIVE_QUERY_STATE: {
|
||||
actions: ['updateQueryStateFromQueryServiceState'],
|
||||
},
|
||||
|
@ -239,8 +244,12 @@ export const createPureLogsExplorerControllerStateMachine = (
|
|||
}
|
||||
: {}
|
||||
),
|
||||
resetRows: actions.assign((_context, event) => ({
|
||||
rows: [],
|
||||
})),
|
||||
notifyDataViewUpdate: raise('DATA_VIEW_UPDATED'),
|
||||
updateContextFromDiscoverAppState,
|
||||
updateContextFromDiscoverDataState,
|
||||
updateDiscoverAppStateFromContext,
|
||||
updateContextFromTimefilter,
|
||||
},
|
||||
|
|
|
@ -7,8 +7,13 @@
|
|||
|
||||
import { ControlGroupAPI } from '@kbn/controls-plugin/public';
|
||||
import { QueryState, RefreshInterval, TimeRange } from '@kbn/data-plugin/common';
|
||||
import { DiscoverAppState, DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||
import type {
|
||||
DiscoverAppState,
|
||||
DiscoverStateContainer,
|
||||
DataDocumentsMsg,
|
||||
} from '@kbn/discover-plugin/public';
|
||||
import { DoneInvokeEvent } from 'xstate';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/src/types';
|
||||
import { ControlPanels, DisplayOptions } from '../../../../common';
|
||||
import type { DatasetEncodingError, DatasetSelection } from '../../../../common/dataset_selection';
|
||||
|
||||
|
@ -32,9 +37,14 @@ export interface WithDiscoverStateContainer {
|
|||
discoverStateContainer: DiscoverStateContainer;
|
||||
}
|
||||
|
||||
export interface WithDataTableRecord {
|
||||
rows: DataTableRecord[];
|
||||
}
|
||||
|
||||
export type DefaultLogsExplorerControllerState = WithDatasetSelection &
|
||||
WithQueryState &
|
||||
WithDisplayOptions;
|
||||
WithDisplayOptions &
|
||||
WithDataTableRecord;
|
||||
|
||||
export type LogsExplorerControllerTypeState =
|
||||
| {
|
||||
|
@ -59,6 +69,7 @@ export type LogsExplorerControllerTypeState =
|
|||
WithControlPanels &
|
||||
WithQueryState &
|
||||
WithDisplayOptions &
|
||||
WithDataTableRecord &
|
||||
WithDiscoverStateContainer;
|
||||
}
|
||||
| {
|
||||
|
@ -67,6 +78,7 @@ export type LogsExplorerControllerTypeState =
|
|||
WithControlPanels &
|
||||
WithQueryState &
|
||||
WithDisplayOptions &
|
||||
WithDataTableRecord &
|
||||
WithDiscoverStateContainer;
|
||||
}
|
||||
| {
|
||||
|
@ -75,6 +87,7 @@ export type LogsExplorerControllerTypeState =
|
|||
WithControlPanels &
|
||||
WithQueryState &
|
||||
WithDisplayOptions &
|
||||
WithDataTableRecord &
|
||||
WithDiscoverStateContainer;
|
||||
}
|
||||
| {
|
||||
|
@ -83,6 +96,7 @@ export type LogsExplorerControllerTypeState =
|
|||
WithControlPanels &
|
||||
WithQueryState &
|
||||
WithDisplayOptions &
|
||||
WithDataTableRecord &
|
||||
WithDiscoverStateContainer;
|
||||
}
|
||||
| {
|
||||
|
@ -91,6 +105,7 @@ export type LogsExplorerControllerTypeState =
|
|||
WithControlPanels &
|
||||
WithQueryState &
|
||||
WithDisplayOptions &
|
||||
WithDataTableRecord &
|
||||
WithDiscoverStateContainer;
|
||||
}
|
||||
| {
|
||||
|
@ -99,6 +114,7 @@ export type LogsExplorerControllerTypeState =
|
|||
WithControlPanels &
|
||||
WithQueryState &
|
||||
WithDisplayOptions &
|
||||
WithDataTableRecord &
|
||||
WithDiscoverStateContainer;
|
||||
}
|
||||
| {
|
||||
|
@ -108,6 +124,7 @@ export type LogsExplorerControllerTypeState =
|
|||
WithControlPanels &
|
||||
WithQueryState &
|
||||
WithDisplayOptions &
|
||||
WithDataTableRecord &
|
||||
WithDiscoverStateContainer;
|
||||
}
|
||||
| {
|
||||
|
@ -117,6 +134,7 @@ export type LogsExplorerControllerTypeState =
|
|||
WithControlPanels &
|
||||
WithQueryState &
|
||||
WithDisplayOptions &
|
||||
WithDataTableRecord &
|
||||
WithDiscoverStateContainer;
|
||||
};
|
||||
|
||||
|
@ -151,6 +169,10 @@ export type LogsExplorerControllerEvent =
|
|||
type: 'RECEIVE_DISCOVER_APP_STATE';
|
||||
appState: DiscoverAppState;
|
||||
}
|
||||
| {
|
||||
type: 'RECEIVE_DISCOVER_DATA_STATE';
|
||||
dataState: DataDocumentsMsg['result'];
|
||||
}
|
||||
| {
|
||||
type: 'RECEIVE_TIMEFILTER_TIME';
|
||||
time: TimeRange;
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { LogDocument } from '../../common/document';
|
||||
|
||||
type Field = keyof LogDocument['flattened'];
|
||||
|
||||
export const getFieldFromDoc = <T extends Field>(doc: LogDocument, field: T) => {
|
||||
const fieldValueArray = doc.flattened[field];
|
||||
return fieldValueArray && fieldValueArray.length ? fieldValueArray[0] : undefined;
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { LogDocument, StackTraceFields } from '../../common/document';
|
||||
import * as constants from '../../common/constants';
|
||||
import { getFieldFromDoc } from './get_field_from_flattened_doc';
|
||||
|
||||
export const getStacktraceFields = (doc: LogDocument): StackTraceFields => {
|
||||
const errorStackTrace = getFieldFromDoc(doc, constants.ERROR_STACK_TRACE);
|
||||
const errorExceptionStackTrace = getFieldFromDoc(doc, constants.ERROR_EXCEPTION_STACKTRACE);
|
||||
const errorLogStackTrace = getFieldFromDoc(doc, constants.ERROR_LOG_STACKTRACE);
|
||||
|
||||
return {
|
||||
[constants.ERROR_STACK_TRACE]: errorStackTrace,
|
||||
[constants.ERROR_EXCEPTION_STACKTRACE]: errorExceptionStackTrace,
|
||||
[constants.ERROR_LOG_STACKTRACE]: errorLogStackTrace,
|
||||
};
|
||||
};
|
|
@ -7,13 +7,7 @@
|
|||
|
||||
import { LogDocument, ResourceFields } from '../../common/document';
|
||||
import * as constants from '../../common/constants';
|
||||
|
||||
type Field = keyof LogDocument['flattened'];
|
||||
|
||||
const getFieldFromDoc = <T extends Field>(doc: LogDocument, field: T) => {
|
||||
const fieldValueArray = doc.flattened[field];
|
||||
return fieldValueArray && fieldValueArray.length ? fieldValueArray[0] : undefined;
|
||||
};
|
||||
import { getFieldFromDoc } from './get_field_from_flattened_doc';
|
||||
|
||||
export const getUnformattedResourceFields = (doc: LogDocument): ResourceFields => {
|
||||
const serviceName = getFieldFromDoc(doc, constants.SERVICE_NAME_FIELD);
|
||||
|
|
|
@ -77,7 +77,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
describe('render content virtual column properly', async () => {
|
||||
it('should render log level and log message when present', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('info')).to.be(true);
|
||||
expect(cellValue.includes('A sample log')).to.be(true);
|
||||
|
@ -86,7 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render log message when present and skip log level when missing', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(1, 4);
|
||||
const cellElement = await dataGrid.getCellElement(1, 3);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('info')).to.be(false);
|
||||
expect(cellValue.includes('A sample log')).to.be(true);
|
||||
|
@ -95,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render message from error object when top level message not present', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(2, 4);
|
||||
const cellElement = await dataGrid.getCellElement(2, 3);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('info')).to.be(true);
|
||||
expect(cellValue.includes('error.message')).to.be(true);
|
||||
|
@ -105,7 +105,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render message from event.original when top level message and error.message not present', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(3, 4);
|
||||
const cellElement = await dataGrid.getCellElement(3, 3);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('info')).to.be(true);
|
||||
expect(cellValue.includes('event.original')).to.be(true);
|
||||
|
@ -115,7 +115,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render the whole JSON when neither message, error.message and event.original are present', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(4, 4);
|
||||
const cellElement = await dataGrid.getCellElement(4, 3);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('info')).to.be(true);
|
||||
|
||||
|
@ -131,7 +131,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('on cell expansion with no message field should open JSON Viewer', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
await dataGrid.clickCellExpandButton(4, 4);
|
||||
await dataGrid.clickCellExpandButton(4, 3);
|
||||
await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover');
|
||||
});
|
||||
});
|
||||
|
@ -139,7 +139,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
it('on cell expansion with message field should open regular popover', async () => {
|
||||
await navigateToLogsExplorer();
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
await dataGrid.clickCellExpandButton(3, 4);
|
||||
await dataGrid.clickCellExpandButton(3, 3);
|
||||
await testSubjects.existOrFail('euiDataGridExpansionPopover');
|
||||
});
|
||||
});
|
||||
|
@ -148,7 +148,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
describe('render resource virtual column properly', async () => {
|
||||
it('should render service name and host name when present', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const cellElement = await dataGrid.getCellElement(0, 2);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('synth-service')).to.be(true);
|
||||
expect(cellValue.includes('synth-host')).to.be(true);
|
||||
|
@ -162,7 +162,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
it('should render a popover with cell actions when a chip on content column is clicked', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const logLevelChip = await cellElement.findByTestSubject(
|
||||
'dataTablePopoverChip_log.level'
|
||||
);
|
||||
|
@ -178,7 +178,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render the table filtered where log.level value is info when filter in action is clicked', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const logLevelChip = await cellElement.findByTestSubject(
|
||||
'dataTablePopoverChip_log.level'
|
||||
);
|
||||
|
@ -198,7 +198,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render the table filtered where log.level value is not info when filter out action is clicked', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const logLevelChip = await cellElement.findByTestSubject(
|
||||
'dataTablePopoverChip_log.level'
|
||||
);
|
||||
|
@ -216,7 +216,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render the table filtered where service.name value is selected', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const cellElement = await dataGrid.getCellElement(0, 2);
|
||||
const serviceNameChip = await cellElement.findByTestSubject(
|
||||
'dataTablePopoverChip_service.name'
|
||||
);
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* 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 { log, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import expect from '@kbn/expect';
|
||||
import moment from 'moment';
|
||||
import { FtrProviderContext } from './config';
|
||||
|
||||
const MORE_THAN_1024_CHARS =
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const retry = getService('retry');
|
||||
const PageObjects = getPageObjects(['discover', 'observabilityLogsExplorer']);
|
||||
const synthtrace = getService('logSynthtraceEsClient');
|
||||
const dataGrid = getService('dataGrid');
|
||||
const from = '2024-02-06T10:24:14.035Z';
|
||||
const to = '2024-02-06T10:25:14.091Z';
|
||||
const TEST_TIMEOUT = 10 * 1000; // 10 secs
|
||||
|
||||
const navigateToLogsExplorer = () =>
|
||||
PageObjects.observabilityLogsExplorer.navigateTo({
|
||||
pageState: {
|
||||
time: {
|
||||
from,
|
||||
to,
|
||||
mode: 'absolute',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('When the logs explorer loads', () => {
|
||||
before(async () => {
|
||||
await synthtrace.index(generateLogsData({ to }));
|
||||
await navigateToLogsExplorer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await synthtrace.clean();
|
||||
});
|
||||
|
||||
describe('should render custom control columns properly', async () => {
|
||||
it('should render control column with proper header', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
// First control column has no title, so empty string, last control column has title
|
||||
expect(await dataGrid.getControlColumnHeaderFields()).to.eql(['', 'actions']);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the expand icon in the last control column', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const expandButton = await cellElement.findByTestSubject('docTableExpandToggleColumn');
|
||||
expect(expandButton).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the malformed icon in the last control column if malformed doc exists', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(1, 4);
|
||||
const malformedButton = await cellElement.findByTestSubject('docTableMalformedDocExist');
|
||||
expect(malformedButton).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the disabled malformed icon in the last control column when malformed doc does not exists', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const malformedDisableButton = await cellElement.findByTestSubject(
|
||||
'docTableMalformedDocDoesNotExist'
|
||||
);
|
||||
expect(malformedDisableButton).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the stacktrace icon in the last control column when stacktrace exists', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(4, 4);
|
||||
const stacktraceButton = await cellElement.findByTestSubject('docTableStacktraceExist');
|
||||
expect(stacktraceButton).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the stacktrace icon disabled in the last control column when stacktrace does not exists', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(1, 4);
|
||||
const stacktraceButton = await cellElement.findByTestSubject(
|
||||
'docTableStacktraceDoesNotExist'
|
||||
);
|
||||
expect(stacktraceButton).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
|
||||
const logs = timerange(moment(to).subtract(1, 'second'), moment(to))
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
Array(count)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return log
|
||||
.create()
|
||||
.message('A sample log')
|
||||
.logLevel('info')
|
||||
.timestamp(timestamp)
|
||||
.defaults({ 'service.name': 'synth-service' });
|
||||
})
|
||||
);
|
||||
|
||||
const malformedDocs = timerange(
|
||||
moment(to).subtract(2, 'second'),
|
||||
moment(to).subtract(1, 'second')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
Array(count)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return log
|
||||
.create()
|
||||
.message('A malformed doc')
|
||||
.logLevel(MORE_THAN_1024_CHARS)
|
||||
.timestamp(timestamp)
|
||||
.defaults({ 'service.name': 'synth-service' });
|
||||
})
|
||||
);
|
||||
|
||||
const logsWithErrorMessage = timerange(
|
||||
moment(to).subtract(3, 'second'),
|
||||
moment(to).subtract(2, 'second')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
Array(count)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return log.create().logLevel('info').timestamp(timestamp).defaults({
|
||||
'error.stack_trace': 'Error message in error.stack_trace',
|
||||
'service.name': 'node-service',
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const logsWithErrorException = timerange(
|
||||
moment(to).subtract(4, 'second'),
|
||||
moment(to).subtract(3, 'second')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
Array(count)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return log.create().logLevel('info').timestamp(timestamp).defaults({
|
||||
'error.exception.stacktrace': 'Error message in error.exception.stacktrace',
|
||||
'service.name': 'node-service',
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const logsWithErrorInLog = timerange(
|
||||
moment(to).subtract(5, 'second'),
|
||||
moment(to).subtract(4, 'second')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
Array(count)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return log.create().logLevel('info').timestamp(timestamp).defaults({
|
||||
'error.log.stacktrace': 'Error message in error.log.stacktrace',
|
||||
'service.name': 'node-service',
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return [logs, malformedDocs, logsWithErrorMessage, logsWithErrorException, logsWithErrorInLog];
|
||||
}
|
|
@ -66,30 +66,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should mount the flyout customization content', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutDetail');
|
||||
});
|
||||
|
||||
it('should display a timestamp badge', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutLogTimestamp');
|
||||
});
|
||||
|
||||
it('should display a log level badge when available', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutLogLevel');
|
||||
await dataGrid.closeFlyout();
|
||||
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutLogLevel');
|
||||
});
|
||||
|
||||
it('should display a message code block when available', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutLogMessage');
|
||||
await dataGrid.closeFlyout();
|
||||
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutLogMessage');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -87,7 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the service container with all fields', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutService');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutTrace');
|
||||
|
@ -98,7 +98,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the service container even when 1 field is missing', async () => {
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutService');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutTrace');
|
||||
|
@ -109,7 +109,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should not load the service container if all fields are missing', async () => {
|
||||
await dataGrid.clickRowToggle({ rowIndex: 2 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 });
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
|
||||
await dataGrid.closeFlyout();
|
||||
});
|
||||
|
@ -155,7 +155,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the cloud container with all fields', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutCloudProvider');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutCloudRegion');
|
||||
|
@ -166,7 +166,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the cloud container even when some fields are missing', async () => {
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud');
|
||||
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider');
|
||||
|
@ -179,7 +179,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should not load the cloud container if all fields are missing', async () => {
|
||||
await dataGrid.clickRowToggle({ rowIndex: 2 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 });
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionCloud');
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider');
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutCloudRegion');
|
||||
|
@ -225,7 +225,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the other container with all fields', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutLogPathFile');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutNamespace');
|
||||
|
@ -235,7 +235,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the other container even when some fields are missing', async () => {
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther');
|
||||
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutLogPathFile');
|
||||
|
|
|
@ -17,5 +17,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./flyout'));
|
||||
loadTestFile(require.resolve('./header_menu'));
|
||||
loadTestFile(require.resolve('./flyout_highlights.ts'));
|
||||
loadTestFile(require.resolve('./custom_control_columns.ts'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
describe('render content virtual column properly', async () => {
|
||||
it('should render log level and log message when present', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('info')).to.be(true);
|
||||
expect(cellValue.includes('A sample log')).to.be(true);
|
||||
|
@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render log message when present and skip log level when missing', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(1, 4);
|
||||
const cellElement = await dataGrid.getCellElement(1, 3);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('info')).to.be(false);
|
||||
expect(cellValue.includes('A sample log')).to.be(true);
|
||||
|
@ -97,7 +97,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render message from error object when top level message not present', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(2, 4);
|
||||
const cellElement = await dataGrid.getCellElement(2, 3);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('info')).to.be(true);
|
||||
expect(cellValue.includes('error.message')).to.be(true);
|
||||
|
@ -107,7 +107,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render message from event.original when top level message and error.message not present', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(3, 4);
|
||||
const cellElement = await dataGrid.getCellElement(3, 3);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('info')).to.be(true);
|
||||
expect(cellValue.includes('event.original')).to.be(true);
|
||||
|
@ -117,7 +117,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render the whole JSON when neither message, error.message and event.original are present', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(4, 4);
|
||||
const cellElement = await dataGrid.getCellElement(4, 3);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('info')).to.be(true);
|
||||
|
||||
|
@ -133,7 +133,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('on cell expansion with no message field should open JSON Viewer', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
await dataGrid.clickCellExpandButton(4, 4);
|
||||
await dataGrid.clickCellExpandButton(4, 3);
|
||||
await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover');
|
||||
});
|
||||
});
|
||||
|
@ -141,7 +141,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
it('on cell expansion with message field should open regular popover', async () => {
|
||||
await navigateToLogsExplorer();
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
await dataGrid.clickCellExpandButton(3, 4);
|
||||
await dataGrid.clickCellExpandButton(3, 3);
|
||||
await testSubjects.existOrFail('euiDataGridExpansionPopover');
|
||||
});
|
||||
});
|
||||
|
@ -150,7 +150,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
describe('render resource virtual column properly', async () => {
|
||||
it('should render service name and host name when present', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const cellElement = await dataGrid.getCellElement(0, 2);
|
||||
const cellValue = await cellElement.getVisibleText();
|
||||
expect(cellValue.includes('synth-service')).to.be(true);
|
||||
expect(cellValue.includes('synth-host')).to.be(true);
|
||||
|
@ -164,7 +164,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
it('should render a popover with cell actions when a chip on content column is clicked', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const logLevelChip = await cellElement.findByTestSubject(
|
||||
'dataTablePopoverChip_log.level'
|
||||
);
|
||||
|
@ -180,7 +180,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render the table filtered where log.level value is info when filter in action is clicked', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const logLevelChip = await cellElement.findByTestSubject(
|
||||
'dataTablePopoverChip_log.level'
|
||||
);
|
||||
|
@ -200,7 +200,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render the table filtered where log.level value is not info when filter out action is clicked', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const logLevelChip = await cellElement.findByTestSubject(
|
||||
'dataTablePopoverChip_log.level'
|
||||
);
|
||||
|
@ -218,7 +218,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should render the table filtered where service.name value is selected', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 3);
|
||||
const cellElement = await dataGrid.getCellElement(0, 2);
|
||||
const serviceNameChip = await cellElement.findByTestSubject(
|
||||
'dataTablePopoverChip_service.name'
|
||||
);
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { log, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import expect from '@kbn/expect';
|
||||
import moment from 'moment';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const MORE_THAN_1024_CHARS =
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const retry = getService('retry');
|
||||
const PageObjects = getPageObjects(['discover', 'observabilityLogsExplorer', 'svlCommonPage']);
|
||||
const synthtrace = getService('svlLogsSynthtraceClient');
|
||||
const dataGrid = getService('dataGrid');
|
||||
const from = '2024-02-06T10:24:14.035Z';
|
||||
const to = '2024-02-06T10:25:14.091Z';
|
||||
const TEST_TIMEOUT = 10 * 1000; // 10 secs
|
||||
|
||||
const navigateToLogsExplorer = () =>
|
||||
PageObjects.observabilityLogsExplorer.navigateTo({
|
||||
pageState: {
|
||||
time: {
|
||||
from,
|
||||
to,
|
||||
mode: 'absolute',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('When the logs explorer loads', () => {
|
||||
before(async () => {
|
||||
await synthtrace.index(generateLogsData({ to }));
|
||||
await PageObjects.svlCommonPage.login();
|
||||
await navigateToLogsExplorer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await synthtrace.clean();
|
||||
await PageObjects.svlCommonPage.forceLogout();
|
||||
});
|
||||
|
||||
describe('should render custom control columns properly', async () => {
|
||||
it('should render control column with proper header', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
// First control column has no title, so empty string, last control column has title
|
||||
expect(await dataGrid.getControlColumnHeaderFields()).to.eql(['', 'actions']);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the expand icon in the last control column', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const expandButton = await cellElement.findByTestSubject('docTableExpandToggleColumn');
|
||||
expect(expandButton).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the malformed icon in the last control column if malformed doc exists', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(1, 4);
|
||||
const malformedButton = await cellElement.findByTestSubject('docTableMalformedDocExist');
|
||||
expect(malformedButton).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the disabled malformed icon in the last control column when malformed doc does not exists', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(0, 4);
|
||||
const malformedDisableButton = await cellElement.findByTestSubject(
|
||||
'docTableMalformedDocDoesNotExist'
|
||||
);
|
||||
expect(malformedDisableButton).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the stacktrace icon in the last control column when stacktrace exists', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(4, 4);
|
||||
const stacktraceButton = await cellElement.findByTestSubject('docTableStacktraceExist');
|
||||
expect(stacktraceButton).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the stacktrace icon disabled in the last control column when stacktrace does not exists', async () => {
|
||||
await retry.tryForTime(TEST_TIMEOUT, async () => {
|
||||
const cellElement = await dataGrid.getCellElement(1, 4);
|
||||
const stacktraceButton = await cellElement.findByTestSubject(
|
||||
'docTableStacktraceDoesNotExist'
|
||||
);
|
||||
expect(stacktraceButton).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
|
||||
const logs = timerange(moment(to).subtract(1, 'second'), moment(to))
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
Array(count)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return log
|
||||
.create()
|
||||
.message('A sample log')
|
||||
.logLevel('info')
|
||||
.timestamp(timestamp)
|
||||
.defaults({ 'service.name': 'synth-service' });
|
||||
})
|
||||
);
|
||||
|
||||
const malformedDocs = timerange(
|
||||
moment(to).subtract(2, 'second'),
|
||||
moment(to).subtract(1, 'second')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
Array(count)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return log
|
||||
.create()
|
||||
.message('A malformed doc')
|
||||
.logLevel(MORE_THAN_1024_CHARS)
|
||||
.timestamp(timestamp)
|
||||
.defaults({ 'service.name': 'synth-service' });
|
||||
})
|
||||
);
|
||||
|
||||
const logsWithErrorMessage = timerange(
|
||||
moment(to).subtract(3, 'second'),
|
||||
moment(to).subtract(2, 'second')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
Array(count)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return log.create().logLevel('info').timestamp(timestamp).defaults({
|
||||
'error.stack_trace': 'Error message in error.stack_trace',
|
||||
'service.name': 'node-service',
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const logsWithErrorException = timerange(
|
||||
moment(to).subtract(4, 'second'),
|
||||
moment(to).subtract(3, 'second')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
Array(count)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return log.create().logLevel('info').timestamp(timestamp).defaults({
|
||||
'error.exception.stacktrace': 'Error message in error.exception.stacktrace',
|
||||
'service.name': 'node-service',
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const logsWithErrorInLog = timerange(
|
||||
moment(to).subtract(5, 'second'),
|
||||
moment(to).subtract(4, 'second')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
Array(count)
|
||||
.fill(0)
|
||||
.map(() => {
|
||||
return log.create().logLevel('info').timestamp(timestamp).defaults({
|
||||
'error.log.stacktrace': 'Error message in error.log.stacktrace',
|
||||
'service.name': 'node-service',
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return [logs, malformedDocs, logsWithErrorMessage, logsWithErrorException, logsWithErrorInLog];
|
||||
}
|
|
@ -68,30 +68,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should mount the flyout customization content', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutDetail');
|
||||
});
|
||||
|
||||
it('should display a timestamp badge', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutLogTimestamp');
|
||||
});
|
||||
|
||||
it('should display a log level badge when available', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutLogLevel');
|
||||
await dataGrid.closeFlyout();
|
||||
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutLogLevel');
|
||||
});
|
||||
|
||||
it('should display a message code block when available', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutLogMessage');
|
||||
await dataGrid.closeFlyout();
|
||||
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutLogMessage');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -89,7 +89,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the service container with all fields', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutService');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutTrace');
|
||||
|
@ -100,7 +100,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the service container even when 1 field is missing', async () => {
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutService');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutTrace');
|
||||
|
@ -111,7 +111,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should not load the service container if all fields are missing', async () => {
|
||||
await dataGrid.clickRowToggle({ rowIndex: 2 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 });
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
|
||||
await dataGrid.closeFlyout();
|
||||
});
|
||||
|
@ -159,7 +159,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the cloud container with all fields', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutCloudProvider');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutCloudRegion');
|
||||
|
@ -170,7 +170,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the cloud container even when some fields are missing', async () => {
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud');
|
||||
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider');
|
||||
|
@ -183,7 +183,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should not load the cloud container if all fields are missing', async () => {
|
||||
await dataGrid.clickRowToggle({ rowIndex: 2 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 });
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionCloud');
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider');
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutCloudRegion');
|
||||
|
@ -231,7 +231,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the other container with all fields', async () => {
|
||||
await dataGrid.clickRowToggle();
|
||||
await dataGrid.clickRowToggle({ columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutLogPathFile');
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutNamespace');
|
||||
|
@ -241,7 +241,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should load the other container even when some fields are missing', async () => {
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1 });
|
||||
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
|
||||
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther');
|
||||
|
||||
await testSubjects.missingOrFail('logsExplorerFlyoutLogPathFile');
|
||||
|
|
|
@ -17,5 +17,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./flyout'));
|
||||
loadTestFile(require.resolve('./header_menu'));
|
||||
loadTestFile(require.resolve('./flyout_highlights.ts'));
|
||||
loadTestFile(require.resolve('./custom_control_columns.ts'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue