[Reporting] Use updated saved object resolver (#113641)

* Update deprecated PDF job factory to use updated saved object resolver
* Reuse data plugin service to gather index pattern attributes in the deprecated CSV export type
This commit is contained in:
Michael Dokolin 2021-10-21 16:14:20 +02:00 committed by GitHub
parent cc3f601bd5
commit 6b64103333
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 78 additions and 82 deletions

View file

@ -307,6 +307,16 @@ export class ReportingCore {
return await this.getUiSettingsServiceFactory(savedObjectsClient);
}
public async getDataViewsService(request: KibanaRequest) {
const { savedObjects } = await this.getPluginStartDeps();
const savedObjectsClient = savedObjects.getScopedClient(request);
const { indexPatterns } = await this.getDataService();
const { asCurrentUser: esClient } = (await this.getEsClient()).asScoped(request);
const dataViews = await indexPatterns.dataViewsServiceFactory(savedObjectsClient, esClient);
return dataViews;
}
public async getDataService() {
const startDeps = await this.getPluginStartDeps();
return startDeps.data;

View file

@ -6,29 +6,18 @@
*/
import { CreateJobFn, CreateJobFnFactory } from '../../types';
import {
IndexPatternSavedObjectDeprecatedCSV,
JobParamsDeprecatedCSV,
TaskPayloadDeprecatedCSV,
} from './types';
import { JobParamsDeprecatedCSV, TaskPayloadDeprecatedCSV } from './types';
export const createJobFnFactory: CreateJobFnFactory<
CreateJobFn<JobParamsDeprecatedCSV, TaskPayloadDeprecatedCSV>
> = function createJobFactoryFn(_reporting, logger) {
> = function createJobFactoryFn(reporting, logger) {
return async function createJob(jobParams, context) {
logger.warn(
`The "/generate/csv" endpoint is deprecated and will be removed in Kibana 8.0. Please recreate the POST URL used to automate this CSV export.`
`The "/generate/csv" endpoint is deprecated. Please recreate the POST URL used to automate this CSV export.`
);
const savedObjectsClient = context.core.savedObjects.client;
const indexPatternSavedObject = (await savedObjectsClient.get(
'index-pattern',
jobParams.indexPatternId
)) as unknown as IndexPatternSavedObjectDeprecatedCSV;
return {
isDeprecated: true,
indexPatternSavedObject,
...jobParams,
};
};

View file

@ -12,6 +12,7 @@ import { ElasticsearchClient, IUiSettingsClient } from 'kibana/server';
import moment from 'moment';
import Puid from 'puid';
import sinon from 'sinon';
import type { DataView, DataViewsService } from 'src/plugins/data/common';
import { ReportingConfig, ReportingCore } from '../../';
import {
FieldFormatsRegistry,
@ -56,6 +57,8 @@ describe('CSV Execute Job', function () {
let encryptedHeaders: any;
let configGetStub: any;
let mockDataView: jest.Mocked<DataView>;
let mockDataViewsService: jest.Mocked<DataViewsService>;
let mockEsClient: DeeplyMockedKeys<ElasticsearchClient>;
let mockReportingConfig: ReportingConfig;
let mockReportingCore: ReportingCore;
@ -81,10 +84,15 @@ describe('CSV Execute Job', function () {
configGetStub.withArgs('csv', 'maxSizeBytes').returns(1024 * 1000); // 1mB
configGetStub.withArgs('csv', 'scroll').returns({});
mockReportingConfig = { get: configGetStub, kbnConfig: { get: configGetStub } };
mockDataView = { fieldFormatMap: {}, fields: [] } as unknown as typeof mockDataView;
mockDataViewsService = {
get: jest.fn().mockResolvedValue(mockDataView),
} as unknown as typeof mockDataViewsService;
mockReportingCore = await createMockReportingCore(createMockConfigSchema());
mockReportingCore.getUiSettingsServiceFactory = () =>
Promise.resolve(mockUiSettingsClient as unknown as IUiSettingsClient);
mockReportingCore.getDataViewsService = jest.fn().mockResolvedValue(mockDataViewsService);
mockReportingCore.setConfig(mockReportingConfig);
mockEsClient = (await mockReportingCore.getEsClient()).asScoped({} as any)
@ -931,16 +939,14 @@ describe('CSV Execute Job', function () {
fields: ['one', 'two'],
conflictedTypesFields: [],
searchRequest: { index: null, body: null },
indexPatternSavedObject: {
id: 'logstash-*',
type: 'index-pattern',
attributes: {
title: 'logstash-*',
fields: '[{"name":"one","type":"string"}, {"name":"two","type":"string"}]',
fieldFormatMap: '{"one":{"id":"string","params":{"transform": "upper"}}}',
},
},
indexPatternId: 'something',
});
mockDataView.fieldFormatMap = { one: { id: 'string', params: { transform: 'upper' } } };
mockDataView.fields = [
{ name: 'one', type: 'string' },
{ name: 'two', type: 'string' },
] as typeof mockDataView.fields;
await runTask('job123', jobParams, cancellationToken, stream);
expect(content).not.toBe(null);
const lines = content!.split('\n');

View file

@ -25,12 +25,14 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadDeprecatedC
const fakeRequest = reporting.getFakeRequest({ headers }, job.spaceId, logger);
const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest, logger);
const { asCurrentUser: elasticsearchClient } = elasticsearch.asScoped(fakeRequest);
const dataViews = await reporting.getDataViewsService(fakeRequest);
const { maxSizeReached, csvContainsFormulas, warnings } = await generateCsv(
job,
config,
uiSettingsClient,
elasticsearchClient,
dataViews,
cancellationToken,
stream
);

View file

@ -6,6 +6,7 @@
*/
import expect from '@kbn/expect';
import type { DataView } from 'src/plugins/data/common';
import {
FieldFormatsGetConfigFn,
FieldFormatsRegistry,
@ -13,20 +14,18 @@ import {
NumberFormat,
FORMATS_UI_SETTINGS,
} from 'src/plugins/field_formats/common';
import { IndexPatternSavedObjectDeprecatedCSV } from '../types';
import { fieldFormatMapFactory } from './field_format_map';
type ConfigValue = { number: { id: string; params: {} } } | string;
describe('field format map', function () {
const indexPatternSavedObject: IndexPatternSavedObjectDeprecatedCSV = {
timeFieldName: '@timestamp',
title: 'logstash-*',
attributes: {
fields: '[{"name":"field1","type":"number"}, {"name":"field2","type":"number"}]',
fieldFormatMap: '{"field1":{"id":"bytes","params":{"pattern":"0,0.[0]b"}}}',
},
};
const dataView = {
fields: [
{ name: 'field1', type: 'number' },
{ name: 'field2', type: 'number' },
],
fieldFormatMap: { field1: { id: 'bytes', params: { pattern: '0,0.[0]b' } } },
} as unknown as DataView;
const configMock: Record<string, ConfigValue> = {};
configMock[FORMATS_UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP] = {
number: { id: 'number', params: {} },
@ -39,13 +38,9 @@ describe('field format map', function () {
const fieldFormatsRegistry = new FieldFormatsRegistry();
fieldFormatsRegistry.init(getConfig, {}, [BytesFormat, NumberFormat]);
const formatMap = fieldFormatMapFactory(
indexPatternSavedObject,
fieldFormatsRegistry,
mockTimezone
);
const formatMap = fieldFormatMapFactory(dataView, fieldFormatsRegistry, mockTimezone);
it('should build field format map with entry per index pattern field', function () {
it('should build field format map with entry per data view field', function () {
expect(formatMap.has('field1')).to.be(true);
expect(formatMap.has('field2')).to.be(true);
expect(formatMap.has('field_not_in_index')).to.be(false);

View file

@ -6,22 +6,21 @@
*/
import _ from 'lodash';
import type { DataView, KBN_FIELD_TYPES } from 'src/plugins/data/common';
import {
FieldFormat,
IFieldFormatsRegistry,
FieldFormatConfig,
} from 'src/plugins/field_formats/common';
import { IndexPatternSavedObjectDeprecatedCSV } from '../types';
/**
* Create a map of FieldFormat instances for index pattern fields
*
* @param {Object} indexPatternSavedObject
* @param {DataView} dataView
* @param {FieldFormatsService} fieldFormats
* @return {Map} key: field name, value: FieldFormat instance
*/
export function fieldFormatMapFactory(
indexPatternSavedObject: IndexPatternSavedObjectDeprecatedCSV,
dataView: DataView | undefined,
fieldFormatsRegistry: IFieldFormatsRegistry,
timezone: string | undefined
) {
@ -32,10 +31,9 @@ export function fieldFormatMapFactory(
const serverDateParams = { timezone };
// Add FieldFormat instances for fields with custom formatters
if (_.has(indexPatternSavedObject, 'attributes.fieldFormatMap')) {
const fieldFormatMap = JSON.parse(indexPatternSavedObject.attributes.fieldFormatMap);
Object.keys(fieldFormatMap).forEach((fieldName) => {
const formatConfig: FieldFormatConfig = fieldFormatMap[fieldName];
if (dataView) {
Object.keys(dataView.fieldFormatMap).forEach((fieldName) => {
const formatConfig: FieldFormatConfig = dataView.fieldFormatMap[fieldName];
const formatParams = {
...formatConfig.params,
...serverDateParams,
@ -48,12 +46,11 @@ export function fieldFormatMapFactory(
}
// Add default FieldFormat instances for non-custom formatted fields
const indexFields = JSON.parse(_.get(indexPatternSavedObject, 'attributes.fields', '[]'));
indexFields.forEach((field: any) => {
dataView?.fields.forEach((field) => {
if (!formatsMap.has(field.name)) {
formatsMap.set(
field.name,
fieldFormatsRegistry.getDefaultInstance(field.type, [], serverDateParams)
fieldFormatsRegistry.getDefaultInstance(field.type as KBN_FIELD_TYPES, [], serverDateParams)
);
}
});

View file

@ -8,6 +8,7 @@
import { Writable } from 'stream';
import { i18n } from '@kbn/i18n';
import { ElasticsearchClient, IUiSettingsClient } from 'src/core/server';
import type { DataView, DataViewsService } from 'src/plugins/data/common';
import { ReportingConfig } from '../../../';
import { createEscapeValue } from '../../../../../../../src/plugins/data/common';
import { CancellationToken } from '../../../../../../plugins/reporting/common';
@ -16,10 +17,7 @@ import { byteSizeValueToNumber } from '../../../../common/schema_utils';
import { LevelLogger } from '../../../lib';
import { getFieldFormats } from '../../../services';
import { MaxSizeStringBuilder } from '../../csv_searchsource/generate_csv/max_size_string_builder';
import {
IndexPatternSavedObjectDeprecatedCSV,
SavedSearchGeneratorResultDeprecatedCSV,
} from '../types';
import { SavedSearchGeneratorResultDeprecatedCSV } from '../types';
import { checkIfRowsHaveFormulas } from './check_cells_for_formulas';
import { fieldFormatMapFactory } from './field_format_map';
import { createFlattenHit } from './flatten_hit';
@ -44,7 +42,7 @@ interface SearchRequest {
export interface GenerateCsvParams {
browserTimezone?: string;
searchRequest: SearchRequest;
indexPatternSavedObject: IndexPatternSavedObjectDeprecatedCSV;
indexPatternId: string;
fields: string[];
metaFields: string[];
conflictedTypesFields: string[];
@ -58,6 +56,7 @@ export function createGenerateCsv(logger: LevelLogger) {
config: ReportingConfig,
uiSettingsClient: IUiSettingsClient,
elasticsearchClient: ElasticsearchClient,
dataViews: DataViewsService,
cancellationToken: CancellationToken,
stream: Writable
): Promise<SavedSearchGeneratorResultDeprecatedCSV> {
@ -91,11 +90,17 @@ export function createGenerateCsv(logger: LevelLogger) {
let csvContainsFormulas = false;
const flattenHit = createFlattenHit(fields, metaFields, conflictedTypesFields);
let dataView: DataView | undefined;
try {
dataView = await dataViews.get(job.indexPatternId);
} catch (error) {
logger.error(`Failed to get the data view "${job.indexPatternId}": ${error}`);
}
const formatsMap = await getFieldFormats()
.fieldFormatServiceFactory(uiSettingsClient)
.then((fieldFormats) =>
fieldFormatMapFactory(job.indexPatternSavedObject, fieldFormats, settings.timezone)
);
.then((fieldFormats) => fieldFormatMapFactory(dataView, fieldFormats, settings.timezone));
const formatCsvValues = createFormatCsvValues(
escapeValue,

View file

@ -5,20 +5,11 @@
* 2.0.
*/
import type { FieldSpec } from 'src/plugins/data/common';
import { BaseParams, BasePayload } from '../../types';
export type RawValue = string | object | null | undefined;
export interface IndexPatternSavedObjectDeprecatedCSV {
title: string;
timeFieldName: string;
fields?: any[];
attributes: {
fields: string;
fieldFormatMap: string;
};
}
interface BaseParamsDeprecatedCSV {
searchRequest: SearchRequestDeprecatedCSV;
fields: string[];
@ -31,10 +22,9 @@ export type JobParamsDeprecatedCSV = BaseParamsDeprecatedCSV &
indexPatternId: string;
};
// CSV create job method converts indexPatternID to indexPatternSavedObject
export type TaskPayloadDeprecatedCSV = BaseParamsDeprecatedCSV &
BasePayload & {
indexPatternSavedObject: IndexPatternSavedObjectDeprecatedCSV;
indexPatternId: string;
};
export interface SearchRequestDeprecatedCSV {

View file

@ -48,11 +48,11 @@ test(`passes title through if provided`, async () => {
test(`gets the title from the savedObject`, async () => {
const createJobMock = jest.fn();
const title = 'savedTitle';
mockRequestHandlerContext.core.savedObjects.client.get.mockResolvedValue(
createMockSavedObject({
mockRequestHandlerContext.core.savedObjects.client.resolve.mockResolvedValue({
saved_object: createMockSavedObject({
attributes: { title },
})
);
}),
} as any);
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ objectType: 'search', savedObjectId: 'abc' }),
@ -72,9 +72,9 @@ test(`gets the title from the savedObject`, async () => {
test(`passes the objectType and savedObjectId to the savedObjectsClient`, async () => {
const createJobMock = jest.fn();
const context = mockRequestHandlerContext;
context.core.savedObjects.client.get.mockResolvedValue(
createMockSavedObject({ attributes: { title: '' } })
);
context.core.savedObjects.client.resolve.mockResolvedValue({
saved_object: createMockSavedObject({ attributes: { title: '' } }),
} as any);
const objectType = 'search';
const savedObjectId = 'abc';
@ -92,10 +92,8 @@ test(`passes the objectType and savedObjectId to the savedObjectsClient`, async
);
expect(mockLogger.error.mock.calls.length).toBe(0);
const getMock = context.core.savedObjects.client.get.mock;
expect(getMock.calls.length).toBe(1);
expect(getMock.calls[0][0]).toBe(objectType);
expect(getMock.calls[0][1]).toBe(savedObjectId);
expect(context.core.savedObjects.client.resolve).toHaveBeenCalledTimes(1);
expect(context.core.savedObjects.client.resolve).toHaveBeenCalledWith(objectType, savedObjectId);
});
test(`logs no warnings when title and relativeUrls is passed`, async () => {

View file

@ -22,7 +22,10 @@ const getSavedObjectTitle = async (
savedObjectId: string,
savedObjectsClient: SavedObjectsClientContract
) => {
const savedObject = await savedObjectsClient.get<{ title: string }>(objectType, savedObjectId);
const { saved_object: savedObject } = await savedObjectsClient.resolve<{ title: string }>(
objectType,
savedObjectId
);
return savedObject.attributes.title;
};

View file

@ -71,6 +71,7 @@ describe('Handle request to generate', () => {
(report) => new Report({ ...report, _index: '.reporting-foo-index-234' })
),
} as unknown as ReportingStore);
mockRequest = getMockRequest();
mockResponseFactory = getMockResponseFactory();
@ -80,6 +81,7 @@ describe('Handle request to generate', () => {
mockContext = getMockContext();
mockContext.reporting = {} as ReportingSetup;
requestHandler = new RequestHandler(
reportingCore,
{ username: 'testymcgee' },
@ -195,7 +197,6 @@ describe('Handle request to generate', () => {
"output": Object {},
"payload": Object {
"browserTimezone": "UTC",
"indexPatternSavedObject": undefined,
"isDeprecated": true,
"layout": Object {
"id": "preserve_layout",