[Logs Explorer] Fix logic to use fallback for Discover link (#176320)

## Summary

Closes https://github.com/elastic/kibana/issues/175127

### Demo

![Discover
Link](87b23726-c574-473b-8af6-6210643bc6f1)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: jennypavlova <jennypavlova94@gmail.com>
This commit is contained in:
Achyut Jhunjhunwala 2024-02-09 21:28:00 +01:00 committed by GitHub
parent 4b72c3343f
commit 80bc424c6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 171 additions and 52 deletions

View file

@ -27,6 +27,22 @@ export type ListFilterControl = {
export const LOGS_EXPLORER_LOCATOR_ID = 'LOGS_EXPLORER_LOCATOR';
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type DocumentFieldGridColumnOptions = {
type: 'document-field';
field: string;
width?: number;
};
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type SmartFieldGridColumnOptions = {
type: 'smart-field';
smartField: 'content' | 'resource';
width?: number;
};
export type GridColumnDisplayOptions = DocumentFieldGridColumnOptions | SmartFieldGridColumnOptions;
export interface LogsExplorerNavigationParams extends SerializableRecord {
/**
* Optionally set the time range in the time picker.
@ -43,7 +59,7 @@ export interface LogsExplorerNavigationParams extends SerializableRecord {
/**
* Columns displayed in the table
*/
columns?: string[];
columns?: GridColumnDisplayOptions[];
/**
* Optionally apply free-form filters.
*/

View file

@ -5,15 +5,15 @@
* 2.0.
*/
import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
import { Aggregators } from './types';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { LogsExplorerLocatorParams } from '@kbn/deeplinks-observability';
import { getViewInAppUrl, GetViewInAppUrlArgs } from './get_view_in_app_url';
describe('getViewInAppUrl', () => {
const logsExplorerLocator = {
getRedirectUrl: jest.fn(() => 'mockedGetRedirectUrl'),
} as unknown as LocatorPublic<DiscoverAppLocatorParams>;
} as unknown as LocatorPublic<LogsExplorerLocatorParams>;
const startedAt = '2023-12-07T16:30:15.403Z';
const endedAt = '2023-12-07T20:30:15.403Z';
const returnedTimeRange = {

View file

@ -6,9 +6,9 @@
*/
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
import type { TimeRange } from '@kbn/es-query';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import { LogsExplorerLocatorParams } from '@kbn/deeplinks-observability';
import type { CustomThresholdExpressionMetric } from './types';
export interface GetViewInAppUrlArgs {
@ -16,7 +16,7 @@ export interface GetViewInAppUrlArgs {
endedAt?: string;
startedAt?: string;
filter?: string;
logsExplorerLocator?: LocatorPublic<DiscoverAppLocatorParams>;
logsExplorerLocator?: LocatorPublic<LogsExplorerLocatorParams>;
metrics?: CustomThresholdExpressionMetric[];
}

View file

@ -14,8 +14,8 @@ import {
ALERT_START,
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
} from '@kbn/rule-data-utils';
import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import { LogsExplorerLocatorParams } from '@kbn/deeplinks-observability';
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import type { MetricExpression } from '../components/custom_threshold/types';
import type {
@ -91,7 +91,7 @@ const getDataViewId = (searchConfiguration?: SerializedSearchSourceFields) =>
export const registerObservabilityRuleTypes = async (
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry,
uiSettings: IUiSettingsClient,
logsExplorerLocator?: LocatorPublic<DiscoverAppLocatorParams>
logsExplorerLocator?: LocatorPublic<LogsExplorerLocatorParams>
) => {
observabilityRuleTypeRegistry.register({
id: SLO_BURN_RATE_RULE_TYPE_ID,

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { SmartFieldGridColumnOptions } from './display_options';
export const LOGS_EXPLORER_PROFILE_ID = 'logs-explorer';
// Fields constants
@ -50,14 +52,25 @@ export const RESOURCE_FIELD = 'resource';
export const DATA_GRID_COLUMN_WIDTH_SMALL = 240;
export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320;
export const ACTIONS_COLUMN_WIDTH = 80;
export const RESOURCE_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = {
type: 'smart-field',
smartField: RESOURCE_FIELD,
fallbackFields: [HOST_NAME_FIELD, SERVICE_NAME_FIELD],
width: DATA_GRID_COLUMN_WIDTH_MEDIUM,
};
export const CONTENT_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = {
type: 'smart-field',
smartField: CONTENT_FIELD,
fallbackFields: [MESSAGE_FIELD],
};
export const SMART_FALLBACK_FIELDS = {
[CONTENT_FIELD]: CONTENT_FIELD_CONFIGURATION,
[RESOURCE_FIELD]: RESOURCE_FIELD_CONFIGURATION,
};
// UI preferences
export const DEFAULT_COLUMNS = [
{
field: RESOURCE_FIELD,
width: DATA_GRID_COLUMN_WIDTH_MEDIUM,
},
{
field: CONTENT_FIELD,
},
];
export const DEFAULT_COLUMNS = [RESOURCE_FIELD_CONFIGURATION, CONTENT_FIELD_CONFIGURATION];
export const DEFAULT_ROWS_PER_PAGE = 100;

View file

@ -11,11 +11,21 @@ export interface ChartDisplayOptions {
export type PartialChartDisplayOptions = Partial<ChartDisplayOptions>;
export interface GridColumnDisplayOptions {
export interface DocumentFieldGridColumnOptions {
type: 'document-field';
field: string;
width?: number;
}
export interface SmartFieldGridColumnOptions {
type: 'smart-field';
smartField: 'content' | 'resource';
fallbackFields: string[];
width?: number;
}
export type GridColumnDisplayOptions = DocumentFieldGridColumnOptions | SmartFieldGridColumnOptions;
export interface GridRowsDisplayOptions {
rowHeight: number;
rowsPerPage: number;

View file

@ -30,3 +30,10 @@ export type {
PartialGridDisplayOptions,
PartialGridRowsDisplayOptions,
} from './display_options';
export {
CONTENT_FIELD,
CONTENT_FIELD_CONFIGURATION,
RESOURCE_FIELD_CONFIGURATION,
SMART_FALLBACK_FIELDS,
} from './constants';

View file

@ -21,7 +21,7 @@ export class LogsExplorerLocatorDefinition implements LocatorDefinition<LogsExpl
constructor(protected readonly deps: LogsExplorerLocatorDependencies) {}
public readonly getLocation = (params: LogsExplorerLocatorParams) => {
const { dataset } = params;
const { dataset, columns } = params;
const dataViewSpec: DataViewSpec | undefined = dataset
? {
id: dataset,
@ -29,8 +29,13 @@ export class LogsExplorerLocatorDefinition implements LocatorDefinition<LogsExpl
}
: undefined;
const discoverColumns = columns?.map((column) => {
return column.type === 'document-field' ? column.field : column.smartField;
});
return this.deps.discoverAppLocator?.getLocation({
...params,
columns: discoverColumns,
dataViewId: dataset,
dataViewSpec,
})!;

View file

@ -78,6 +78,7 @@ export function ChipWithPopover({
font-size: ${xsFontSize};
display: flex;
justify-content: center;
${shouldRenderPopover && `margin-right: 4px; margin-top: -3px;`}
cursor: pointer;
`}
style={style}

View file

@ -7,7 +7,6 @@
import React from 'react';
import type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
import { first } from 'lodash';
import { AgentName } from '@kbn/elastic-agent-utils';
import { dynamic } from '@kbn/shared-ux-utility';
import { ChipWithPopover } from '../common/popover_chip';
@ -27,10 +26,12 @@ export const Resource = ({ row }: DataGridCellValueElementProps) => {
text={resourceDoc[constants.SERVICE_NAME_FIELD] as string}
rightSideIcon="arrowDown"
leftSideIcon={
<AgentIcon
agentName={first((resourceDoc[constants.AGENT_NAME_FIELD] ?? []) as AgentName[])}
size="m"
/>
resourceDoc[constants.AGENT_NAME_FIELD] && (
<AgentIcon
agentName={resourceDoc[constants.AGENT_NAME_FIELD] as AgentName}
size="m"
/>
)
}
/>
)}

View file

@ -25,6 +25,7 @@ export {
getDiscoverColumnsFromDisplayOptions,
getDiscoverGridFromDisplayOptions,
getDiscoverFiltersFromState,
getDiscoverColumnsWithFallbackFieldsFromDisplayOptions,
} from './utils/convert_discover_app_state';
export function plugin(context: PluginInitializerContext<LogsExplorerConfig>) {

View file

@ -10,6 +10,7 @@ import { DiscoverAppState } from '@kbn/discover-plugin/public';
import { ExistsFilter, Filter, FILTERS, PhrasesFilter } from '@kbn/es-query';
import { PhraseFilterValue } from '@kbn/es-query/src/filters/build_filters';
import { cloneDeep } from 'lodash';
import { CONTENT_FIELD, RESOURCE_FIELD, SMART_FALLBACK_FIELDS } from '../../common/constants';
import {
ChartDisplayOptions,
DisplayOptions,
@ -21,10 +22,16 @@ import type { ControlOptions, OptionsListControl } from '../controller';
export const getGridColumnDisplayOptionsFromDiscoverAppState = (
discoverAppState: DiscoverAppState
): GridColumnDisplayOptions[] | undefined =>
discoverAppState.columns?.map((field) => ({
field,
width: discoverAppState.grid?.columns?.[field]?.width,
}));
discoverAppState.columns?.map((field) => {
if (field === CONTENT_FIELD || field === RESOURCE_FIELD) {
return SMART_FALLBACK_FIELDS[field];
}
return {
type: 'document-field',
field,
width: discoverAppState.grid?.columns?.[field]?.width,
};
});
export const getGridRowsDisplayOptionsFromDiscoverAppState = (
discoverAppState: DiscoverAppState
@ -58,18 +65,32 @@ export const getDiscoverAppStateFromContext = (
filters: cloneDeep(displayOptions.filters),
});
export const getDiscoverColumnsWithFallbackFieldsFromDisplayOptions = (
displayOptions: DisplayOptions
): DiscoverAppState['columns'] =>
displayOptions.grid.columns.flatMap((column) => {
return column.type === 'document-field'
? column.field
: SMART_FALLBACK_FIELDS[column.smartField].fallbackFields;
});
export const getDiscoverColumnsFromDisplayOptions = (
displayOptions: DisplayOptions
): DiscoverAppState['columns'] => displayOptions.grid.columns.map(({ field }) => field);
): DiscoverAppState['columns'] =>
displayOptions.grid.columns.flatMap((column) => {
return column.type === 'document-field' ? column.field : column.smartField;
});
export const getDiscoverGridFromDisplayOptions = (
displayOptions: DisplayOptions
): DiscoverAppState['grid'] => ({
columns: displayOptions.grid.columns.reduce<
NonNullable<NonNullable<DiscoverAppState['grid']>['columns']>
>((gridColumns, { field, width }) => {
if (width != null) {
gridColumns[field] = { width };
>((gridColumns, column) => {
const key = column.type === 'document-field' ? column.field : column.smartField;
if (column.width != null) {
gridColumns[key] = { width: column.width };
}
return gridColumns;
}, {}),

View file

@ -92,9 +92,9 @@ describe('Observability Logs Explorer Locators', () => {
});
});
it('should allow specifiying columns', async () => {
it('should allow specifying columns', async () => {
const params: AllDatasetsLocatorParams = {
columns: ['_source'],
columns: [{ field: '_source', type: 'document-field' }],
};
const { allDatasetsLocator } = await setup();
@ -102,7 +102,7 @@ describe('Observability Logs Explorer Locators', () => {
expect(location).toMatchObject({
app: OBSERVABILITY_LOGS_EXPLORER_APP_ID,
path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selectionType:all),v:1)`,
path: '/?pageState=(columns:!((field:_source,type:document-field)),datasetSelection:(selectionType:all),v:1)',
state: {},
});
});
@ -214,7 +214,7 @@ describe('Observability Logs Explorer Locators', () => {
const params: SingleDatasetLocatorParams = {
integration,
dataset,
columns: ['_source'],
columns: [{ field: '_source', type: 'document-field' }],
};
const { singleDatasetLocator } = await setup();
@ -222,7 +222,7 @@ describe('Observability Logs Explorer Locators', () => {
expect(location).toMatchObject({
app: OBSERVABILITY_LOGS_EXPLORER_APP_ID,
path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`,
path: `/?pageState=(columns:!((field:_source,type:document-field)),datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`,
state: {},
});
});

View file

@ -15,6 +15,7 @@ import {
AvailableControlPanels,
availableControlsPanels,
DatasetSelectionPlain,
SMART_FALLBACK_FIELDS,
} from '@kbn/logs-explorer-plugin/common';
import { OBSERVABILITY_LOGS_EXPLORER_APP_ID } from '@kbn/deeplinks-observability';
import {
@ -46,7 +47,9 @@ export const constructLocatorPath = async (params: LocatorPathConstructionParams
query,
refreshInterval,
time: timeRange,
columns: columns?.map((field) => ({ field })),
columns: columns?.map((column) => {
return column.type === 'smart-field' ? SMART_FALLBACK_FIELDS[column.smartField] : column;
}),
controls: getControlsPageStateFromFilterControlsParams(filterControls ?? {}),
})
);

View file

@ -8,8 +8,15 @@
import { availableControlsPanels, datasetSelectionPlainRT } from '@kbn/logs-explorer-plugin/common';
import * as rt from 'io-ts';
export const columnRT = rt.intersection([
const allowedNamesRT = rt.keyof({
content: null,
resource: null,
});
// Define the runtime type for DocumentFieldGridColumnOptions
const documentFieldColumnRT = rt.intersection([
rt.strict({
type: rt.literal('document-field'),
field: rt.string,
}),
rt.exact(
@ -19,6 +26,22 @@ export const columnRT = rt.intersection([
),
]);
// Define the runtime type for SmartFieldGridColumnOptions
const smartFieldColumnRT = rt.intersection([
rt.strict({
type: rt.literal('smart-field'),
smartField: allowedNamesRT,
fallbackFields: rt.array(rt.string),
}),
rt.exact(
rt.partial({
width: rt.number,
})
),
]);
export const columnRT = rt.union([documentFieldColumnRT, smartFieldColumnRT]);
export const columnsRT = rt.array(columnRT);
export const optionsListControlRT = rt.strict({

View file

@ -10,7 +10,7 @@ import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
import { DiscoverStart } from '@kbn/discover-plugin/public';
import { hydrateDatasetSelection } from '@kbn/logs-explorer-plugin/common';
import {
getDiscoverColumnsFromDisplayOptions,
getDiscoverColumnsWithFallbackFieldsFromDisplayOptions,
getDiscoverFiltersFromState,
} from '@kbn/logs-explorer-plugin/public';
import { getRouterLinkProps } from '@kbn/router-utils';
@ -57,7 +57,7 @@ export const DiscoverLinkForValidState = React.memo(
const index = hydrateDatasetSelection(logsExplorerState.datasetSelection).toDataviewSpec();
return {
breakdownField: logsExplorerState.chart.breakdownField ?? undefined,
columns: getDiscoverColumnsFromDisplayOptions(logsExplorerState),
columns: getDiscoverColumnsWithFallbackFieldsFromDisplayOptions(logsExplorerState),
filters: getDiscoverFiltersFromState(
index.id,
logsExplorerState.filters,

View file

@ -58,9 +58,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
mode: 'absolute',
},
columns: [
{ field: 'resource' },
{ field: 'content' },
{ field: 'data_stream.namespace' },
{
smartField: 'resource',
type: 'smart-field',
fallbackFields: ['host.name', 'service.name'],
},
{
smartField: 'content',
type: 'smart-field',
fallbackFields: ['message'],
},
{ field: 'data_stream.namespace', type: 'document-field' },
],
},
});

View file

@ -46,7 +46,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await discoverLink.isDisplayed()).to.be(true);
});
it('should navigate to discover keeping the current columns/filters/query/time/data view', async () => {
it('should navigate to discover keeping the current filters/query/time/data view and use fallback columns for virtual columns', async () => {
await retry.try(async () => {
await testSubjects.existOrFail('superDatePickerstartDatePopoverButton');
await testSubjects.existOrFail('superDatePickerendDatePopoverButton');
@ -69,8 +69,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
expect(await PageObjects.discover.getColumnHeaders()).to.eql([
'@timestamp',
'resource',
'content',
'host.name',
'service.name',
'message',
]);
});

View file

@ -60,9 +60,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
mode: 'absolute',
},
columns: [
{ field: 'resource' },
{ field: 'content' },
{ field: 'data_stream.namespace' },
{
smartField: 'resource',
type: 'smart-field',
fallbackFields: ['host.name', 'service.name'],
},
{
smartField: 'content',
type: 'smart-field',
fallbackFields: ['message'],
},
{ field: 'data_stream.namespace', type: 'document-field' },
],
},
});

View file

@ -71,7 +71,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await discoverLink.isDisplayed()).to.be(true);
});
it('should navigate to discover keeping the current columns/filters/query/time/data view', async () => {
it('should navigate to discover keeping the current filters/query/time/data view and use fallback columns for virtual columns', async () => {
await retry.try(async () => {
await testSubjects.existOrFail('superDatePickerstartDatePopoverButton');
await testSubjects.existOrFail('superDatePickerendDatePopoverButton');
@ -91,8 +91,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
expect(await PageObjects.discover.getColumnHeaders()).to.eql([
'@timestamp',
'resource',
'content',
'host.name',
'service.name',
'message',
]);
});
await retry.try(async () => {