[Discover] Make share links and search session info shorter for ad-hoc data views (#161180)

- Closes https://github.com/elastic/kibana/issues/160993

## Summary

This PR introduces `dataView.toMinimalSpec()` which is used now in 3
cases:
- when constructing an alert link
- when constructing a share URL for ad-hoc data views
- when constructing search session info for ad-hoc data views

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia Rechkunova 2023-07-25 21:49:13 +02:00 committed by GitHub
parent 618c7fa32b
commit 8b42e0f79b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 189 additions and 50 deletions

View file

@ -114,6 +114,7 @@ export const buildDataViewMock = ({
isTimeNanosBased: () => false,
isPersisted: () => true,
toSpec: () => ({}),
toMinimalSpec: () => ({}),
getTimeField: () => {
return dataViewFields.find((field) => field.name === timeFieldName);
},

View file

@ -26,6 +26,29 @@ Object {
}
`;
exports[`IndexPattern toMinimalSpec can exclude fields 1`] = `
Object {
"allowNoIndex": false,
"fieldAttrs": undefined,
"fieldFormats": Object {},
"id": "test-pattern",
"name": "Name",
"runtimeFieldMap": Object {
"runtime_field": Object {
"script": Object {
"source": "emit('hello world')",
},
"type": "keyword",
},
},
"sourceFilters": Array [],
"timeFieldName": "time",
"title": "title",
"type": "index-pattern",
"version": "2",
}
`;
exports[`IndexPattern toSpec can optionally exclude fields 1`] = `
Object {
"allowNoIndex": false,

View file

@ -478,4 +478,111 @@ describe('IndexPattern', () => {
expect(dataView1.sourceFilters).not.toBe(dataView2.sourceFilters);
});
});
describe('toMinimalSpec', () => {
test('can exclude fields', () => {
expect(indexPattern.toMinimalSpec()).toMatchSnapshot();
});
test('can omit counts', () => {
const fieldsMap = {
test1: {
name: 'test1',
type: 'keyword',
aggregatable: true,
searchable: true,
readFromDocValues: false,
},
test2: {
name: 'test2',
type: 'keyword',
aggregatable: true,
searchable: true,
readFromDocValues: false,
},
test3: {
name: 'test3',
type: 'keyword',
aggregatable: true,
searchable: true,
readFromDocValues: false,
},
};
expect(
create('test', {
id: 'test',
title: 'test*',
fields: fieldsMap,
fieldAttrs: undefined,
}).toMinimalSpec().fieldAttrs
).toBeUndefined();
expect(
create('test', {
id: 'test',
title: 'test*',
fields: fieldsMap,
fieldAttrs: {
test1: {
count: 11,
},
test2: {
count: 12,
},
},
}).toMinimalSpec().fieldAttrs
).toBeUndefined();
expect(
create('test', {
id: 'test',
title: 'test*',
fields: fieldsMap,
fieldAttrs: {
test1: {
count: 11,
customLabel: 'test11',
},
test2: {
count: 12,
},
},
}).toMinimalSpec().fieldAttrs
).toMatchInlineSnapshot(`
Object {
"test1": Object {
"customLabel": "test11",
},
}
`);
expect(
create('test', {
id: 'test',
title: 'test*',
fields: fieldsMap,
fieldAttrs: {
test1: {
count: 11,
customLabel: 'test11',
},
test2: {
customLabel: 'test12',
},
test3: {
count: 30,
},
},
}).toMinimalSpec().fieldAttrs
).toMatchInlineSnapshot(`
Object {
"test1": Object {
"customLabel": "test11",
},
"test2": Object {
"customLabel": "test12",
},
}
`);
});
});
});

View file

@ -15,7 +15,7 @@ import type {
} from '@kbn/field-formats-plugin/common';
import { castEsToKbnFieldTypeName, ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import { CharacterNotAllowedInField } from '@kbn/kibana-utils-plugin/common';
import { cloneDeep, each, reject } from 'lodash';
import { cloneDeep, each, mapValues, omit, pickBy, reject } from 'lodash';
import type { DataViewAttributes, FieldAttrs, FieldAttrSet } from '..';
import type { DataViewField, IIndexPatternFieldList } from '../fields';
import { fieldList } from '../fields';
@ -332,6 +332,28 @@ export class DataView implements DataViewBase {
return Object.fromEntries(Object.entries(spec).filter(([, v]) => typeof v !== 'undefined'));
}
/**
* Creates a minimal static representation of the data view. Fields and popularity scores will be omitted.
*/
public toMinimalSpec(): Omit<DataViewSpec, 'fields'> {
// removes `fields`
const dataViewSpec = this.toSpec(false);
if (dataViewSpec.fieldAttrs) {
// removes `count` props (popularity scores) from `fieldAttrs`
dataViewSpec.fieldAttrs = pickBy(
mapValues(dataViewSpec.fieldAttrs, (fieldAttrs) => omit(fieldAttrs, 'count')),
(trimmedFieldAttrs) => Object.keys(trimmedFieldAttrs).length > 0
);
if (Object.keys(dataViewSpec.fieldAttrs).length === 0) {
dataViewSpec.fieldAttrs = undefined;
}
}
return dataViewSpec;
}
/**
* Get the source filtering configuration for that index.
*/

View file

@ -146,7 +146,7 @@ export const getTopNavLinks = ({
...(savedSearch.id ? { savedSearchId: savedSearch.id } : {}),
...(dataView?.isPersisted()
? { dataViewId: dataView?.id }
: { dataViewSpec: dataView?.toSpec() }),
: { dataViewSpec: dataView?.toMinimalSpec() }),
filters,
timeRange,
refreshInterval,

View file

@ -522,7 +522,7 @@ describe('Test discover state actions', () => {
timeFieldName: 'mock-time-field-name',
};
const dataViewsCreateMock = discoverServiceMock.dataViews.create as jest.Mock;
dataViewsCreateMock.mockImplementation(() => ({
dataViewsCreateMock.mockImplementationOnce(() => ({
...dataViewMock,
...dataViewSpecMock,
isPersisted: () => false,

View file

@ -540,6 +540,6 @@ function createUrlGeneratorState({
viewMode: appState.viewMode,
hideAggregatedPreview: appState.hideAggregatedPreview,
breakdownField: appState.breakdownField,
dataViewSpec: !dataView?.isPersisted() ? dataView?.toSpec(false) : undefined,
dataViewSpec: !dataView?.isPersisted() ? dataView?.toMinimalSpec() : undefined,
};
}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { omit, pickBy, mapValues } from 'lodash';
import { buildRangeFilter, Filter } from '@kbn/es-query';
import {
DataView,
@ -221,19 +220,5 @@ function updateFilterReferences(filters: Filter[], fromDataView: string, toDataV
export function getSmallerDataViewSpec(
dataView: DataView
): DiscoverAppLocatorParams['dataViewSpec'] {
const dataViewSpec = dataView.toSpec(false);
if (dataViewSpec.fieldAttrs) {
// remove `count` props
dataViewSpec.fieldAttrs = pickBy(
mapValues(dataViewSpec.fieldAttrs, (fieldAttrs) => omit(fieldAttrs, 'count')),
(trimmedFieldAttrs) => Object.keys(trimmedFieldAttrs).length > 0
);
if (Object.keys(dataViewSpec.fieldAttrs).length === 0) {
dataViewSpec.fieldAttrs = undefined;
}
}
return dataViewSpec;
return dataView.toMinimalSpec();
}

View file

@ -14,6 +14,8 @@ import {
AlertInstanceMock,
} from '@kbn/alerting-plugin/server/mocks';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import type { DataViewSpec } from '@kbn/data-views-plugin/common';
import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { getRuleType } from './rule_type';
import { EsQueryRuleParams, EsQueryRuleState } from './rule_type_params';
import { ActionContext } from './action_context';
@ -512,32 +514,31 @@ describe('ruleType', () => {
});
describe('search source query', () => {
const dataViewMock = {
id: 'test-id',
title: 'test-title',
timeFieldName: 'time-field',
fields: [
{
name: 'message',
type: 'string',
displayName: 'message',
scripted: false,
filterable: false,
aggregatable: false,
const dataViewMock = createStubDataView({
spec: {
id: 'test-id',
title: 'test-title',
timeFieldName: 'time-field',
fields: {
message: {
name: 'message',
type: 'string',
scripted: false,
searchable: false,
aggregatable: false,
readFromDocValues: false,
},
timestamp: {
name: 'timestamp',
type: 'date',
scripted: false,
searchable: true,
aggregatable: false,
readFromDocValues: false,
},
},
{
name: 'timestamp',
type: 'date',
displayName: 'timestamp',
scripted: false,
filterable: false,
aggregatable: false,
},
],
toSpec: () => {
return { id: 'test-id', title: 'test-title', timeFieldName: 'time-field' };
},
};
});
const defaultParams: OnlySearchSourceRuleParams = {
size: 100,
timeWindowSize: 5,
@ -583,9 +584,9 @@ describe('ruleType', () => {
const searchResult: ESSearchResponse<unknown, {}> = generateResults([]);
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
(ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({
toSpec: () => dataViewMock.toSpec(),
});
(ruleServices.dataViews.create as jest.Mock).mockImplementationOnce((spec: DataViewSpec) =>
createStubDataView({ spec })
);
(searchSourceInstanceMock.getField as jest.Mock).mockImplementation((name: string) => {
if (name === 'index') {
return dataViewMock;
@ -620,9 +621,9 @@ describe('ruleType', () => {
const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] };
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
(ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({
toSpec: () => dataViewMock.toSpec(),
});
(ruleServices.dataViews.create as jest.Mock).mockImplementationOnce((spec: DataViewSpec) =>
createStubDataView({ spec })
);
(searchSourceInstanceMock.getField as jest.Mock).mockImplementation((name: string) => {
if (name === 'index') {
return dataViewMock;