mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Feature/Reporting] Export Saved Search CSV as Dashboard Panel Action (#34571)
This commit is contained in:
parent
2f162d44ef
commit
86c3ac4ff4
64 changed files with 3761 additions and 71 deletions
|
@ -28,7 +28,9 @@ class PanelActionsStore {
|
|||
*/
|
||||
public initializeFromRegistry(panelActionsRegistry: ContextMenuAction[]) {
|
||||
panelActionsRegistry.forEach(panelAction => {
|
||||
this.actions.push(panelAction);
|
||||
if (!this.actions.includes(panelAction)) {
|
||||
this.actions.push(panelAction);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,6 +110,10 @@ export class SearchEmbeddable extends Embeddable {
|
|||
return this.inspectorAdaptors;
|
||||
}
|
||||
|
||||
public getPanelTitle() {
|
||||
return this.panelTitle;
|
||||
}
|
||||
|
||||
public onContainerStateChanged(containerState: ContainerState) {
|
||||
this.customization = containerState.embeddableCustomization || {};
|
||||
this.filters = containerState.filters;
|
||||
|
|
|
@ -25,6 +25,7 @@ Object {
|
|||
"zoom": 2,
|
||||
},
|
||||
"csv": Object {
|
||||
"enablePanelActionDownload": false,
|
||||
"maxSizeBytes": 10485760,
|
||||
"scroll": Object {
|
||||
"duration": "30s",
|
||||
|
@ -85,6 +86,7 @@ Object {
|
|||
"zoom": 2,
|
||||
},
|
||||
"csv": Object {
|
||||
"enablePanelActionDownload": false,
|
||||
"maxSizeBytes": 10485760,
|
||||
"scroll": Object {
|
||||
"duration": "30s",
|
||||
|
@ -144,6 +146,7 @@ Object {
|
|||
"zoom": 2,
|
||||
},
|
||||
"csv": Object {
|
||||
"enablePanelActionDownload": false,
|
||||
"maxSizeBytes": 10485760,
|
||||
"scroll": Object {
|
||||
"duration": "30s",
|
||||
|
@ -204,6 +207,7 @@ Object {
|
|||
"zoom": 2,
|
||||
},
|
||||
"csv": Object {
|
||||
"enablePanelActionDownload": false,
|
||||
"maxSizeBytes": 10485760,
|
||||
"scroll": Object {
|
||||
"duration": "30s",
|
||||
|
|
|
@ -7,12 +7,15 @@
|
|||
import { isFunction } from 'lodash';
|
||||
|
||||
export class CancellationToken {
|
||||
private isCancelled: boolean;
|
||||
private _callbacks: any[];
|
||||
|
||||
constructor() {
|
||||
this.isCancelled = false;
|
||||
this._callbacks = [];
|
||||
}
|
||||
|
||||
on = (callback) => {
|
||||
on(callback: Function) {
|
||||
if (!isFunction(callback)) {
|
||||
throw new Error('Expected callback to be a function');
|
||||
}
|
||||
|
@ -23,10 +26,10 @@ export class CancellationToken {
|
|||
}
|
||||
|
||||
this._callbacks.push(callback);
|
||||
};
|
||||
}
|
||||
|
||||
cancel = () => {
|
||||
cancel() {
|
||||
this.isCancelled = true;
|
||||
this._callbacks.forEach(callback => callback());
|
||||
};
|
||||
}
|
||||
}
|
|
@ -9,12 +9,16 @@ export const PLUGIN_ID = 'reporting';
|
|||
export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY =
|
||||
'xpack.reporting.jobCompletionNotifications';
|
||||
|
||||
export const API_BASE_URL = '/api/reporting';
|
||||
export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu
|
||||
export const API_BASE_URL_V1 = '/api/reporting/v1'; //
|
||||
export const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`;
|
||||
|
||||
export const CONTENT_TYPE_CSV = 'text/csv';
|
||||
|
||||
export const WHITELISTED_JOB_CONTENT_TYPES = [
|
||||
'application/json',
|
||||
'application/pdf',
|
||||
'text/csv',
|
||||
CONTENT_TYPE_CSV,
|
||||
'image/png',
|
||||
];
|
||||
|
||||
|
@ -41,4 +45,5 @@ export const KIBANA_REPORTING_TYPE = 'reporting';
|
|||
export const PDF_JOB_TYPE = 'printable_pdf';
|
||||
export const PNG_JOB_TYPE = 'PNG';
|
||||
export const CSV_JOB_TYPE = 'csv';
|
||||
export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject';
|
||||
export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE];
|
||||
|
|
|
@ -15,7 +15,15 @@ beforeEach(() => {
|
|||
test(`fails if no URL is passed`, async () => {
|
||||
await expect(
|
||||
addForceNowQuerystring({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
server: mockServer,
|
||||
})
|
||||
).rejects.toBeDefined();
|
||||
|
@ -24,7 +32,17 @@ test(`fails if no URL is passed`, async () => {
|
|||
test(`adds forceNow to hash's query, if it exists`, async () => {
|
||||
const forceNow = '2000-01-01T00:00:00.000Z';
|
||||
const { urls } = await addForceNowQuerystring({
|
||||
job: { relativeUrl: '/app/kibana#/something', forceNow },
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
relativeUrl: '/app/kibana#/something',
|
||||
forceNow,
|
||||
},
|
||||
server: mockServer,
|
||||
});
|
||||
|
||||
|
@ -38,6 +56,13 @@ test(`appends forceNow to hash's query, if it exists`, async () => {
|
|||
|
||||
const { urls } = await addForceNowQuerystring({
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
relativeUrl: '/app/kibana#/something?_g=something',
|
||||
forceNow,
|
||||
},
|
||||
|
@ -52,6 +77,13 @@ test(`appends forceNow to hash's query, if it exists`, async () => {
|
|||
test(`doesn't append forceNow query to url, if it doesn't exists`, async () => {
|
||||
const { urls } = await addForceNowQuerystring({
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
relativeUrl: '/app/kibana#/something',
|
||||
},
|
||||
server: mockServer,
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
// @ts-ignore
|
||||
|
||||
import url from 'url';
|
||||
import { getAbsoluteUrlFactory } from '../../../common/get_absolute_url';
|
||||
import { ConditionalHeaders, KbnServer, ReportingJob } from '../../../types';
|
||||
import { ConditionalHeaders, JobDocPayload, KbnServer } from '../../../types';
|
||||
|
||||
function getSavedObjectAbsoluteUrl(job: ReportingJob, relativeUrl: string, server: KbnServer) {
|
||||
function getSavedObjectAbsoluteUrl(job: JobDocPayload, relativeUrl: string, server: KbnServer) {
|
||||
const getAbsoluteUrl: any = getAbsoluteUrlFactory(server);
|
||||
|
||||
const { pathname: path, hash, search } = url.parse(relativeUrl);
|
||||
|
@ -21,7 +21,7 @@ export const addForceNowQuerystring = async ({
|
|||
logo,
|
||||
server,
|
||||
}: {
|
||||
job: ReportingJob;
|
||||
job: JobDocPayload;
|
||||
conditionalHeaders?: ConditionalHeaders;
|
||||
logo?: any;
|
||||
server: KbnServer;
|
||||
|
@ -34,7 +34,7 @@ export const addForceNowQuerystring = async ({
|
|||
job.urls = [getSavedObjectAbsoluteUrl(job, job.relativeUrl, server)];
|
||||
}
|
||||
|
||||
const urls = job.urls.map(jobUrl => {
|
||||
const urls = job.urls.map((jobUrl: string) => {
|
||||
if (!job.forceNow) {
|
||||
return jobUrl;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,17 @@ describe('headers', () => {
|
|||
test(`fails if it can't decrypt headers`, async () => {
|
||||
await expect(
|
||||
decryptJobHeaders({
|
||||
job: { relativeUrl: '/app/kibana#/something', timeRange: {} },
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
relativeUrl: '/app/kibana#/something',
|
||||
timeRange: {},
|
||||
},
|
||||
server: mockServer,
|
||||
})
|
||||
).rejects.toBeDefined();
|
||||
|
@ -37,7 +47,17 @@ describe('headers', () => {
|
|||
|
||||
const encryptedHeaders = await encryptHeaders(headers);
|
||||
const { decryptedHeaders } = await decryptJobHeaders({
|
||||
job: { relativeUrl: '/app/kibana#/something', headers: encryptedHeaders },
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
relativeUrl: '/app/kibana#/something',
|
||||
headers: encryptedHeaders,
|
||||
},
|
||||
server: mockServer,
|
||||
});
|
||||
expect(decryptedHeaders).toEqual(headers);
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
*/
|
||||
// @ts-ignore
|
||||
import { cryptoFactory } from '../../../server/lib/crypto';
|
||||
import { CryptoFactory, KbnServer, ReportingJob } from '../../../types';
|
||||
import { CryptoFactory, JobDocPayload, KbnServer } from '../../../types';
|
||||
|
||||
export const decryptJobHeaders = async ({
|
||||
job,
|
||||
server,
|
||||
}: {
|
||||
job: ReportingJob;
|
||||
job: JobDocPayload;
|
||||
server: KbnServer;
|
||||
}) => {
|
||||
const crypto: CryptoFactory = cryptoFactory(server);
|
||||
|
|
|
@ -26,7 +26,15 @@ describe('conditions', () => {
|
|||
};
|
||||
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: permittedHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -43,7 +51,15 @@ describe('conditions', () => {
|
|||
};
|
||||
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: permittedHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -64,7 +80,15 @@ describe('conditions', () => {
|
|||
};
|
||||
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: permittedHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -81,7 +105,15 @@ describe('conditions', () => {
|
|||
};
|
||||
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: permittedHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -96,7 +128,15 @@ describe('conditions', () => {
|
|||
};
|
||||
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: permittedHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -119,7 +159,15 @@ describe('conditions', () => {
|
|||
};
|
||||
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: permittedHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -136,7 +184,15 @@ describe('conditions', () => {
|
|||
};
|
||||
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: permittedHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -152,7 +208,15 @@ test('uses basePath from job when creating saved object service', async () => {
|
|||
};
|
||||
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: permittedHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -162,7 +226,16 @@ test('uses basePath from job when creating saved object service', async () => {
|
|||
|
||||
const jobBasePath = '/sbp/s/marketing';
|
||||
await getCustomLogo({
|
||||
job: { basePath: jobBasePath },
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
basePath: jobBasePath,
|
||||
},
|
||||
conditionalHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -179,7 +252,15 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav
|
|||
};
|
||||
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: permittedHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -188,7 +269,15 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav
|
|||
mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo);
|
||||
|
||||
await getCustomLogo({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
conditionalHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -202,7 +291,15 @@ describe('config formatting', () => {
|
|||
test(`lowercases server.host`, async () => {
|
||||
mockServer = createMockServer({ settings: { 'server.host': 'COOL-HOSTNAME' } });
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: {},
|
||||
server: mockServer,
|
||||
});
|
||||
|
@ -214,7 +311,15 @@ describe('config formatting', () => {
|
|||
settings: { 'xpack.reporting.kibanaServer.hostname': 'GREAT-HOSTNAME' },
|
||||
});
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: {},
|
||||
server: mockServer,
|
||||
});
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { ConditionalHeaders, ConfigObject, KbnServer, ReportingJob } from '../../../types';
|
||||
import { ConditionalHeaders, ConfigObject, JobDocPayload, KbnServer } from '../../../types';
|
||||
|
||||
export const getConditionalHeaders = ({
|
||||
job,
|
||||
filteredHeaders,
|
||||
server,
|
||||
}: {
|
||||
job: ReportingJob;
|
||||
job: JobDocPayload;
|
||||
filteredHeaders: Record<string, string>;
|
||||
server: KbnServer;
|
||||
}) => {
|
||||
|
|
|
@ -19,13 +19,29 @@ test(`gets logo from uiSettings`, async () => {
|
|||
};
|
||||
|
||||
const { conditionalHeaders } = await getConditionalHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
filteredHeaders: permittedHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
||||
const { logo } = await getCustomLogo({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
conditionalHeaders,
|
||||
server: mockServer,
|
||||
});
|
||||
|
|
|
@ -3,15 +3,16 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants';
|
||||
import { ConditionalHeaders, KbnServer, ReportingJob } from '../../../types';
|
||||
import { ConditionalHeaders, JobDocPayload, KbnServer } from '../../../types';
|
||||
|
||||
export const getCustomLogo = async ({
|
||||
job,
|
||||
conditionalHeaders,
|
||||
server,
|
||||
}: {
|
||||
job: ReportingJob;
|
||||
job: JobDocPayload;
|
||||
conditionalHeaders: ConditionalHeaders;
|
||||
server: KbnServer;
|
||||
}) => {
|
||||
|
|
|
@ -27,7 +27,15 @@ test(`omits blacklisted headers`, async () => {
|
|||
};
|
||||
|
||||
const { filteredHeaders } = await omitBlacklistedHeaders({
|
||||
job: {},
|
||||
job: {
|
||||
title: 'cool-job-bro',
|
||||
type: 'csv',
|
||||
jobParams: {
|
||||
savedObjectId: 'abc-123',
|
||||
isImmediate: false,
|
||||
savedObjectType: 'search',
|
||||
},
|
||||
},
|
||||
decryptedHeaders: {
|
||||
...permittedHeaders,
|
||||
...blacklistedHeaders,
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
*/
|
||||
import { omit } from 'lodash';
|
||||
import { KBN_SCREENSHOT_HEADER_BLACKLIST } from '../../../common/constants';
|
||||
import { KbnServer, ReportingJob } from '../../../types';
|
||||
import { JobDocPayload, KbnServer } from '../../../types';
|
||||
|
||||
export const omitBlacklistedHeaders = ({
|
||||
job,
|
||||
decryptedHeaders,
|
||||
server,
|
||||
}: {
|
||||
job: ReportingJob;
|
||||
job: JobDocPayload;
|
||||
decryptedHeaders: Record<string, string>;
|
||||
server: KbnServer;
|
||||
}) => {
|
||||
|
|
|
@ -9,7 +9,7 @@ import Puid from 'puid';
|
|||
import sinon from 'sinon';
|
||||
import nodeCrypto from '@elastic/node-crypto';
|
||||
|
||||
import { CancellationToken } from '../../../../server/lib/esqueue/helpers/cancellation_token';
|
||||
import { CancellationToken } from '../../../../common/cancellation_token';
|
||||
import { FieldFormat } from '../../../../../../../src/legacy/ui/field_formats/field_format.js';
|
||||
import { FieldFormatsService } from '../../../../../../../src/legacy/ui/field_formats/field_formats_service.js';
|
||||
import { createStringFormat } from '../../../../../../../src/legacy/core_plugins/kibana/common/field_formats/types/string.js';
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
|
||||
// TODO this logic should be re-used with Discover
|
||||
export function createFlattenHit(fields, metaFields, conflictedTypesFields) {
|
||||
const flattenSource = (flat, obj, keyPrefix) => {
|
||||
keyPrefix = keyPrefix ? keyPrefix + '.' : '';
|
||||
|
|
150
x-pack/plugins/reporting/export_types/csv_from_savedobject/index.d.ts
vendored
Normal file
150
x-pack/plugins/reporting/export_types/csv_from_savedobject/index.d.ts
vendored
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface SavedObjectServiceError {
|
||||
statusCode: number;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface SavedObjectMetaJSON {
|
||||
searchSourceJSON: string;
|
||||
}
|
||||
|
||||
export interface SavedObjectMeta {
|
||||
searchSource: SearchSource;
|
||||
}
|
||||
|
||||
export interface SavedSearchObjectAttributesJSON {
|
||||
title: string;
|
||||
sort: any[];
|
||||
columns: string[];
|
||||
kibanaSavedObjectMeta: SavedObjectMetaJSON;
|
||||
uiState: any;
|
||||
}
|
||||
|
||||
export interface SavedSearchObjectAttributes {
|
||||
title: string;
|
||||
sort: any[];
|
||||
columns?: string[];
|
||||
kibanaSavedObjectMeta: SavedObjectMeta;
|
||||
uiState: any;
|
||||
}
|
||||
|
||||
export interface VisObjectAttributesJSON {
|
||||
title: string;
|
||||
visState: string; // JSON string
|
||||
type: string;
|
||||
params: any;
|
||||
uiStateJSON: string; // also JSON string
|
||||
aggs: any[];
|
||||
sort: any[];
|
||||
kibanaSavedObjectMeta: SavedObjectMeta;
|
||||
}
|
||||
|
||||
export interface VisObjectAttributes {
|
||||
title: string;
|
||||
visState: string; // JSON string
|
||||
type: string;
|
||||
params: any;
|
||||
uiState: {
|
||||
vis: {
|
||||
params: {
|
||||
sort: {
|
||||
columnIndex: string;
|
||||
direction: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
aggs: any[];
|
||||
sort: any[];
|
||||
kibanaSavedObjectMeta: SavedObjectMeta;
|
||||
}
|
||||
|
||||
export interface SavedObjectReference {
|
||||
name: string; // should be kibanaSavedObjectMeta.searchSourceJSON.index
|
||||
type: string; // should be index-pattern
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface SavedObject {
|
||||
attributes: any;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
/* This object is passed to different helpers in different parts of the code
|
||||
- packages/kbn-es-query/src/es_query/build_es_query
|
||||
- x-pack/plugins/reporting/export_types/csv/server/lib/field_format_map
|
||||
The structure has redundant parts and json-parsed / json-unparsed versions of the same data
|
||||
*/
|
||||
export interface IndexPatternSavedObject {
|
||||
title: string;
|
||||
timeFieldName: string;
|
||||
fields: any[];
|
||||
attributes: {
|
||||
fieldFormatMap: string;
|
||||
fields: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TimeRangeParams {
|
||||
timezone: string;
|
||||
min: Date | string | number;
|
||||
max: Date | string | number;
|
||||
}
|
||||
|
||||
export interface VisPanel {
|
||||
indexPatternSavedObjectId?: string;
|
||||
savedSearchObjectId?: string;
|
||||
attributes: VisObjectAttributes;
|
||||
timerange: TimeRangeParams;
|
||||
}
|
||||
|
||||
export interface SearchPanel {
|
||||
indexPatternSavedObjectId: string;
|
||||
attributes: SavedSearchObjectAttributes;
|
||||
timerange: TimeRangeParams;
|
||||
}
|
||||
|
||||
export interface SearchSourceQuery {
|
||||
isSearchSourceQuery: boolean;
|
||||
}
|
||||
|
||||
export interface SearchSource {
|
||||
query: SearchSourceQuery;
|
||||
filter: any[];
|
||||
}
|
||||
|
||||
export interface SearchRequest {
|
||||
index: string;
|
||||
body:
|
||||
| {
|
||||
_source: {
|
||||
excludes: string[];
|
||||
includes: string[];
|
||||
};
|
||||
docvalue_fields: string[];
|
||||
query:
|
||||
| {
|
||||
bool: {
|
||||
filter: any[];
|
||||
must_not: any[];
|
||||
should: any[];
|
||||
must: any[];
|
||||
};
|
||||
}
|
||||
| any;
|
||||
script_fields: any;
|
||||
sort: Array<{
|
||||
[key: string]: {
|
||||
order: string;
|
||||
};
|
||||
}>;
|
||||
stored_fields: string[];
|
||||
}
|
||||
| any;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './index.d';
|
||||
|
||||
/*
|
||||
* These functions are exported to share with the API route handler that
|
||||
* generates csv from saved object immediately on request.
|
||||
*/
|
||||
export { executeJobFactory } from './server/execute_job';
|
||||
export { createJobFactory } from './server/create_job';
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants';
|
||||
|
||||
export const metadata = {
|
||||
id: CSV_FROM_SAVEDOBJECT_JOB_TYPE,
|
||||
name: CSV_FROM_SAVEDOBJECT_JOB_TYPE,
|
||||
};
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { notFound, notImplemented } from 'boom';
|
||||
import { Request } from 'hapi';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { cryptoFactory, LevelLogger, oncePerServer } from '../../../../server/lib';
|
||||
import { JobDocPayload, JobParams, KbnServer } from '../../../../types';
|
||||
import {
|
||||
SavedObject,
|
||||
SavedObjectServiceError,
|
||||
SavedSearchObjectAttributesJSON,
|
||||
SearchPanel,
|
||||
TimeRangeParams,
|
||||
VisObjectAttributesJSON,
|
||||
} from '../../';
|
||||
import { createJobSearch } from './create_job_search';
|
||||
|
||||
interface VisData {
|
||||
title: string;
|
||||
visType: string;
|
||||
panel: SearchPanel;
|
||||
}
|
||||
|
||||
type CreateJobFn = (jobParams: JobParams, headers: any, req: Request) => Promise<JobDocPayload>;
|
||||
|
||||
function createJobFn(server: KbnServer): CreateJobFn {
|
||||
const crypto = cryptoFactory(server);
|
||||
const logger = LevelLogger.createForServer(server, ['reporting', 'savedobject-csv']);
|
||||
|
||||
return async function createJob(
|
||||
jobParams: JobParams,
|
||||
headers: any,
|
||||
req: Request
|
||||
): Promise<JobDocPayload> {
|
||||
const { savedObjectType, savedObjectId } = jobParams;
|
||||
const serializedEncryptedHeaders = await crypto.encrypt(headers);
|
||||
const client = req.getSavedObjectsClient();
|
||||
|
||||
const { panel, title, visType }: VisData = await Promise.resolve()
|
||||
.then(() => client.get(savedObjectType, savedObjectId))
|
||||
.then(async (savedObject: SavedObject) => {
|
||||
const { attributes, references } = savedObject;
|
||||
const {
|
||||
kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON,
|
||||
} = attributes as SavedSearchObjectAttributesJSON;
|
||||
const { timerange } = req.payload as { timerange: TimeRangeParams };
|
||||
|
||||
if (!kibanaSavedObjectMetaJSON) {
|
||||
throw new Error('Could not parse saved object data!');
|
||||
}
|
||||
|
||||
const kibanaSavedObjectMeta = {
|
||||
...kibanaSavedObjectMetaJSON,
|
||||
searchSource: JSON.parse(kibanaSavedObjectMetaJSON.searchSourceJSON),
|
||||
};
|
||||
|
||||
const { visState: visStateJSON } = attributes as VisObjectAttributesJSON;
|
||||
if (visStateJSON) {
|
||||
throw notImplemented('Visualization types are not yet implemented');
|
||||
}
|
||||
|
||||
// saved search type
|
||||
return await createJobSearch(timerange, attributes, references, kibanaSavedObjectMeta);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
const boomErr = (err as unknown) as { isBoom: boolean };
|
||||
if (boomErr.isBoom) {
|
||||
throw err;
|
||||
}
|
||||
const errPayload: SavedObjectServiceError = get(err, 'output.payload', { statusCode: 0 });
|
||||
if (errPayload.statusCode === 404) {
|
||||
throw notFound(errPayload.message);
|
||||
}
|
||||
if (err.stack) {
|
||||
logger.error(err.stack);
|
||||
}
|
||||
throw new Error(`Unable to create a job from saved object data! Error: ${err}`);
|
||||
});
|
||||
|
||||
return {
|
||||
basePath: req.getBasePath(),
|
||||
headers: serializedEncryptedHeaders,
|
||||
jobParams: { ...jobParams, panel, visType },
|
||||
type: null, // resolved in executeJob
|
||||
objects: null, // resolved in executeJob
|
||||
title,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const createJobFactory = oncePerServer(createJobFn);
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
SavedObjectMeta,
|
||||
SavedObjectReference,
|
||||
SavedSearchObjectAttributes,
|
||||
SearchPanel,
|
||||
TimeRangeParams,
|
||||
} from '../../';
|
||||
|
||||
interface SearchPanelData {
|
||||
title: string;
|
||||
visType: string;
|
||||
panel: SearchPanel;
|
||||
}
|
||||
|
||||
export async function createJobSearch(
|
||||
timerange: TimeRangeParams,
|
||||
attributes: SavedSearchObjectAttributes,
|
||||
references: SavedObjectReference[],
|
||||
kibanaSavedObjectMeta: SavedObjectMeta
|
||||
): Promise<SearchPanelData> {
|
||||
const { searchSource } = kibanaSavedObjectMeta;
|
||||
if (!searchSource || !references) {
|
||||
throw new Error('The saved search object is missing configuration fields!');
|
||||
}
|
||||
|
||||
const indexPatternMeta = references.find(
|
||||
(ref: SavedObjectReference) => ref.type === 'index-pattern'
|
||||
);
|
||||
if (!indexPatternMeta) {
|
||||
throw new Error('Could not find index pattern for the saved search!');
|
||||
}
|
||||
|
||||
const sPanel = {
|
||||
attributes: {
|
||||
...attributes,
|
||||
kibanaSavedObjectMeta: { searchSource },
|
||||
},
|
||||
indexPatternSavedObjectId: indexPatternMeta.id,
|
||||
timerange,
|
||||
};
|
||||
|
||||
return { panel: sPanel, title: attributes.title, visType: 'search' };
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { createJobFactory } from './create_job';
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Request } from 'hapi';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { cryptoFactory, LevelLogger, oncePerServer } from '../../../server/lib';
|
||||
import { JobDocOutputExecuted, JobDocPayload, KbnServer } from '../../../types';
|
||||
import { CONTENT_TYPE_CSV } from '../../../common/constants';
|
||||
import { CsvResultFromSearch, createGenerateCsv } from './lib';
|
||||
|
||||
interface FakeRequest {
|
||||
headers: any;
|
||||
getBasePath: (opts: any) => string;
|
||||
server: KbnServer;
|
||||
}
|
||||
|
||||
type ExecuteJobFn = (job: JobDocPayload, realRequest?: Request) => Promise<JobDocOutputExecuted>;
|
||||
|
||||
function executeJobFn(server: KbnServer): ExecuteJobFn {
|
||||
const crypto = cryptoFactory(server);
|
||||
const config = server.config();
|
||||
const serverBasePath = config.get('server.basePath');
|
||||
const logger = LevelLogger.createForServer(server, ['reporting', 'savedobject-csv']);
|
||||
const generateCsv = createGenerateCsv(logger);
|
||||
|
||||
return async function executeJob(
|
||||
job: JobDocPayload,
|
||||
realRequest?: Request
|
||||
): Promise<JobDocOutputExecuted> {
|
||||
const { basePath, jobParams } = job;
|
||||
const { isImmediate, panel, visType } = jobParams;
|
||||
|
||||
logger.debug(`Execute job generating [${visType}] csv`);
|
||||
|
||||
let requestObject: Request | FakeRequest;
|
||||
if (isImmediate && realRequest) {
|
||||
logger.debug(`executing job from immediate API`);
|
||||
requestObject = realRequest;
|
||||
} else {
|
||||
logger.debug(`executing job async using encrypted headers`);
|
||||
let decryptedHeaders;
|
||||
const serializedEncryptedHeaders = job.headers;
|
||||
try {
|
||||
decryptedHeaders = await crypto.decrypt(serializedEncryptedHeaders);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
i18n.translate(
|
||||
'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToDecryptReportJobDataErrorMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}',
|
||||
values: { encryptionKey: 'xpack.reporting.encryptionKey', err },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
requestObject = {
|
||||
headers: decryptedHeaders,
|
||||
getBasePath: () => basePath || serverBasePath,
|
||||
server,
|
||||
};
|
||||
}
|
||||
|
||||
let content: string;
|
||||
let maxSizeReached = false;
|
||||
let size = 0;
|
||||
try {
|
||||
const generateResults: CsvResultFromSearch = await generateCsv(
|
||||
requestObject,
|
||||
server,
|
||||
visType as string,
|
||||
panel,
|
||||
jobParams
|
||||
);
|
||||
|
||||
({
|
||||
result: { content, maxSizeReached, size },
|
||||
} = generateResults);
|
||||
} catch (err) {
|
||||
logger.error(`Generate CSV Error! ${err}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (maxSizeReached) {
|
||||
logger.warn(`Max size reached: CSV output truncated to ${size} bytes`);
|
||||
}
|
||||
|
||||
return {
|
||||
content_type: CONTENT_TYPE_CSV,
|
||||
content,
|
||||
max_size_reached: maxSizeReached,
|
||||
size,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const executeJobFactory = oncePerServer(executeJobFn);
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants';
|
||||
import { ExportTypesRegistry } from '../../../types';
|
||||
import { metadata } from '../metadata';
|
||||
import { createJobFactory } from './create_job';
|
||||
import { executeJobFactory } from './execute_job';
|
||||
|
||||
export function register(registry: ExportTypesRegistry) {
|
||||
registry.register({
|
||||
...metadata,
|
||||
jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE,
|
||||
jobContentExtension: 'csv',
|
||||
createJobFactory,
|
||||
executeJobFactory,
|
||||
validLicenses: ['trial', 'basic', 'standard', 'gold', 'platinum'],
|
||||
});
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { badRequest } from 'boom';
|
||||
import { Request } from 'hapi';
|
||||
import { KbnServer, Logger, JobParams } from '../../../../types';
|
||||
import { SearchPanel, VisPanel } from '../../';
|
||||
import { generateCsvSearch } from './generate_csv_search';
|
||||
|
||||
interface FakeRequest {
|
||||
headers: any;
|
||||
getBasePath: (opts: any) => string;
|
||||
server: KbnServer;
|
||||
}
|
||||
|
||||
export function createGenerateCsv(logger: Logger) {
|
||||
return async function generateCsv(
|
||||
request: Request | FakeRequest,
|
||||
server: KbnServer,
|
||||
visType: string,
|
||||
panel: VisPanel | SearchPanel,
|
||||
jobParams: JobParams
|
||||
) {
|
||||
// This should support any vis type that is able to fetch
|
||||
// and model data on the server-side
|
||||
|
||||
// This structure will not be needed when the vis data just consists of an
|
||||
// expression that we could run through the interpreter to get csv
|
||||
switch (visType) {
|
||||
case 'search':
|
||||
return await generateCsvSearch(
|
||||
request as Request,
|
||||
server,
|
||||
logger,
|
||||
panel as SearchPanel,
|
||||
jobParams
|
||||
);
|
||||
default:
|
||||
throw badRequest(`Unsupported or unrecognized saved object type: ${visType}`);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Request } from 'hapi';
|
||||
|
||||
// @ts-ignore no module definition
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
// @ts-ignore no module definition
|
||||
import { createGenerateCsv } from '../../../csv/server/lib/generate_csv';
|
||||
|
||||
import { CancellationToken } from '../../../../common/cancellation_token';
|
||||
|
||||
import { KbnServer, Logger, JobParams } from '../../../../types';
|
||||
import {
|
||||
IndexPatternSavedObject,
|
||||
SavedSearchObjectAttributes,
|
||||
SearchPanel,
|
||||
SearchRequest,
|
||||
SearchSource,
|
||||
SearchSourceQuery,
|
||||
} from '../../';
|
||||
import {
|
||||
CsvResultFromSearch,
|
||||
ESQueryConfig,
|
||||
GenerateCsvParams,
|
||||
Filter,
|
||||
IndexPatternField,
|
||||
QueryFilter,
|
||||
} from './';
|
||||
import { getDataSource } from './get_data_source';
|
||||
import { getFilters } from './get_filters';
|
||||
|
||||
const getEsQueryConfig = async (config: any) => {
|
||||
const configs = await Promise.all([
|
||||
config.get('query:allowLeadingWildcards'),
|
||||
config.get('query:queryString:options'),
|
||||
config.get('courier:ignoreFilterIfFieldNotInIndex'),
|
||||
]);
|
||||
const [allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex] = configs;
|
||||
return { allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex };
|
||||
};
|
||||
|
||||
const getUiSettings = async (config: any) => {
|
||||
const configs = await Promise.all([config.get('csv:separator'), config.get('csv:quoteValues')]);
|
||||
const [separator, quoteValues] = configs;
|
||||
return { separator, quoteValues };
|
||||
};
|
||||
|
||||
export async function generateCsvSearch(
|
||||
req: Request,
|
||||
server: KbnServer,
|
||||
logger: Logger,
|
||||
searchPanel: SearchPanel,
|
||||
jobParams: JobParams
|
||||
): Promise<CsvResultFromSearch> {
|
||||
const { savedObjects, uiSettingsServiceFactory } = server;
|
||||
const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(req);
|
||||
const { indexPatternSavedObjectId, timerange } = searchPanel;
|
||||
const savedSearchObjectAttr = searchPanel.attributes as SavedSearchObjectAttributes;
|
||||
const { indexPatternSavedObject } = await getDataSource(
|
||||
savedObjectsClient,
|
||||
indexPatternSavedObjectId
|
||||
);
|
||||
const uiConfig = uiSettingsServiceFactory({ savedObjectsClient });
|
||||
const esQueryConfig = await getEsQueryConfig(uiConfig);
|
||||
|
||||
const {
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSource: {
|
||||
filter: [searchSourceFilter],
|
||||
query: searchSourceQuery,
|
||||
},
|
||||
},
|
||||
} = savedSearchObjectAttr as { kibanaSavedObjectMeta: { searchSource: SearchSource } };
|
||||
|
||||
const {
|
||||
timeFieldName: indexPatternTimeField,
|
||||
title: esIndex,
|
||||
fields: indexPatternFields,
|
||||
} = indexPatternSavedObject;
|
||||
|
||||
let payloadQuery: QueryFilter | undefined;
|
||||
let payloadSort: any[] = [];
|
||||
if (jobParams.post && jobParams.post.state) {
|
||||
({
|
||||
post: { state: { query: payloadQuery, sort: payloadSort = [] } },
|
||||
} = jobParams);
|
||||
}
|
||||
|
||||
const { includes, timezone, combinedFilter } = getFilters(
|
||||
indexPatternSavedObjectId,
|
||||
indexPatternTimeField,
|
||||
timerange,
|
||||
savedSearchObjectAttr,
|
||||
searchSourceFilter,
|
||||
payloadQuery
|
||||
);
|
||||
|
||||
const [savedSortField, savedSortOrder] = savedSearchObjectAttr.sort;
|
||||
const sortConfig = [...payloadSort, { [savedSortField]: { order: savedSortOrder } }];
|
||||
|
||||
const scriptFieldsConfig = indexPatternFields
|
||||
.filter((f: IndexPatternField) => f.scripted)
|
||||
.reduce((accum: any, curr: IndexPatternField) => {
|
||||
return {
|
||||
...accum,
|
||||
[curr.name]: {
|
||||
script: {
|
||||
source: curr.script,
|
||||
lang: curr.lang,
|
||||
},
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
const docValueFields = indexPatternTimeField ? [indexPatternTimeField] : undefined;
|
||||
|
||||
// this array helps ensure the params are passed to buildEsQuery (non-Typescript) in the right order
|
||||
const buildCsvParams: [IndexPatternSavedObject, SearchSourceQuery, Filter[], ESQueryConfig] = [
|
||||
indexPatternSavedObject,
|
||||
searchSourceQuery,
|
||||
combinedFilter,
|
||||
esQueryConfig,
|
||||
];
|
||||
|
||||
const searchRequest: SearchRequest = {
|
||||
index: esIndex,
|
||||
body: {
|
||||
_source: { includes },
|
||||
docvalue_fields: docValueFields,
|
||||
query: buildEsQuery(...buildCsvParams),
|
||||
script_fields: scriptFieldsConfig,
|
||||
sort: sortConfig,
|
||||
},
|
||||
};
|
||||
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
|
||||
const callCluster = (...params: any[]) => callWithRequest(req, ...params);
|
||||
const config = server.config();
|
||||
const uiSettings = await getUiSettings(uiConfig);
|
||||
|
||||
const generateCsvParams: GenerateCsvParams = {
|
||||
searchRequest,
|
||||
callEndpoint: callCluster,
|
||||
fields: includes,
|
||||
formatsMap: new Map(), // there is no field formatting in this API; this is required for generateCsv
|
||||
metaFields: [],
|
||||
conflictedTypesFields: [],
|
||||
cancellationToken: new CancellationToken(),
|
||||
settings: {
|
||||
...uiSettings,
|
||||
maxSizeBytes: config.get('xpack.reporting.csv.maxSizeBytes'),
|
||||
scroll: config.get('xpack.reporting.csv.scroll'),
|
||||
timezone,
|
||||
},
|
||||
};
|
||||
|
||||
const generateCsv = createGenerateCsv(logger);
|
||||
|
||||
return {
|
||||
type: 'CSV from Saved Search',
|
||||
result: await generateCsv(generateCsvParams),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
IndexPatternSavedObject,
|
||||
SavedObjectReference,
|
||||
SavedSearchObjectAttributesJSON,
|
||||
SearchSource,
|
||||
} from '../../';
|
||||
|
||||
export async function getDataSource(
|
||||
savedObjectsClient: any,
|
||||
indexPatternId?: string,
|
||||
savedSearchObjectId?: string
|
||||
): Promise<{
|
||||
indexPatternSavedObject: IndexPatternSavedObject;
|
||||
searchSource: SearchSource | null;
|
||||
}> {
|
||||
let indexPatternSavedObject: IndexPatternSavedObject;
|
||||
let searchSource: SearchSource | null = null;
|
||||
|
||||
if (savedSearchObjectId) {
|
||||
try {
|
||||
const { attributes, references } = (await savedObjectsClient.get(
|
||||
'search',
|
||||
savedSearchObjectId
|
||||
)) as { attributes: SavedSearchObjectAttributesJSON; references: SavedObjectReference[] };
|
||||
searchSource = JSON.parse(attributes.kibanaSavedObjectMeta.searchSourceJSON);
|
||||
const { id: indexPatternFromSearchId } = references.find(
|
||||
({ type }) => type === 'index-pattern'
|
||||
) as { id: string };
|
||||
({ indexPatternSavedObject } = await getDataSource(
|
||||
savedObjectsClient,
|
||||
indexPatternFromSearchId
|
||||
));
|
||||
return { searchSource, indexPatternSavedObject };
|
||||
} catch (err) {
|
||||
throw new Error(`Could not get saved search info! ${err}`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const { attributes } = await savedObjectsClient.get('index-pattern', indexPatternId);
|
||||
const { fields, title, timeFieldName } = attributes;
|
||||
const parsedFields = fields ? JSON.parse(fields) : [];
|
||||
|
||||
indexPatternSavedObject = {
|
||||
fields: parsedFields,
|
||||
title,
|
||||
timeFieldName,
|
||||
attributes,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`Could not get index pattern saved object! ${err}`);
|
||||
}
|
||||
return { indexPatternSavedObject, searchSource };
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedSearchObjectAttributes, TimeRangeParams } from '../../';
|
||||
import { QueryFilter, SearchSourceFilter } from './';
|
||||
import { getFilters } from './get_filters';
|
||||
|
||||
interface Args {
|
||||
indexPatternId: string;
|
||||
indexPatternTimeField: string | null;
|
||||
timerange: TimeRangeParams | null;
|
||||
savedSearchObjectAttr: SavedSearchObjectAttributes;
|
||||
searchSourceFilter: SearchSourceFilter;
|
||||
queryFilter: QueryFilter;
|
||||
}
|
||||
|
||||
describe('CSV from Saved Object: get_filters', () => {
|
||||
let args: Args;
|
||||
beforeEach(() => {
|
||||
args = {
|
||||
indexPatternId: 'logs-test-*',
|
||||
indexPatternTimeField: 'testtimestamp',
|
||||
timerange: {
|
||||
timezone: 'UTC',
|
||||
min: '1901-01-01T00:00:00.000Z',
|
||||
max: '1902-01-01T00:00:00.000Z',
|
||||
},
|
||||
savedSearchObjectAttr: {
|
||||
title: 'test',
|
||||
sort: [{ sortField: { order: 'asc' } }],
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSource: {
|
||||
query: { isSearchSourceQuery: true },
|
||||
filter: ['hello searchSource filter 1'],
|
||||
},
|
||||
},
|
||||
columns: ['larry'],
|
||||
uiState: null,
|
||||
},
|
||||
searchSourceFilter: { isSearchSourceFilter: true, isFilter: true },
|
||||
queryFilter: { isQueryFilter: true, isFilter: true },
|
||||
};
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('for timebased search', () => {
|
||||
const filters = getFilters(
|
||||
args.indexPatternId,
|
||||
args.indexPatternTimeField,
|
||||
args.timerange,
|
||||
args.savedSearchObjectAttr,
|
||||
args.searchSourceFilter,
|
||||
args.queryFilter
|
||||
);
|
||||
|
||||
expect(filters).toEqual({
|
||||
combinedFilter: [
|
||||
{
|
||||
range: {
|
||||
testtimestamp: {
|
||||
format: 'strict_date_time',
|
||||
gte: '1901-01-01T00:00:00Z',
|
||||
lte: '1902-01-01T00:00:00Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ isFilter: true, isSearchSourceFilter: true },
|
||||
{ isFilter: true, isQueryFilter: true },
|
||||
],
|
||||
includes: ['testtimestamp', 'larry'],
|
||||
timezone: 'UTC',
|
||||
});
|
||||
});
|
||||
|
||||
it('for non-timebased search', () => {
|
||||
args.indexPatternTimeField = null;
|
||||
args.timerange = null;
|
||||
|
||||
const filters = getFilters(
|
||||
args.indexPatternId,
|
||||
args.indexPatternTimeField,
|
||||
args.timerange,
|
||||
args.savedSearchObjectAttr,
|
||||
args.searchSourceFilter,
|
||||
args.queryFilter
|
||||
);
|
||||
|
||||
expect(filters).toEqual({
|
||||
combinedFilter: [
|
||||
{ isFilter: true, isSearchSourceFilter: true },
|
||||
{ isFilter: true, isQueryFilter: true },
|
||||
],
|
||||
includes: ['larry'],
|
||||
timezone: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
it('throw if timebased and timerange is missing', () => {
|
||||
args.timerange = null;
|
||||
|
||||
const throwFn = () =>
|
||||
getFilters(
|
||||
args.indexPatternId,
|
||||
args.indexPatternTimeField,
|
||||
args.timerange,
|
||||
args.savedSearchObjectAttr,
|
||||
args.searchSourceFilter,
|
||||
args.queryFilter
|
||||
);
|
||||
|
||||
expect(throwFn).toThrow(
|
||||
'Time range params are required for index pattern [logs-test-*], using time field [testtimestamp]'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('composes the defined filters', () => {
|
||||
expect(
|
||||
getFilters(
|
||||
args.indexPatternId,
|
||||
args.indexPatternTimeField,
|
||||
args.timerange,
|
||||
args.savedSearchObjectAttr,
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
).toEqual({
|
||||
combinedFilter: [
|
||||
{
|
||||
range: {
|
||||
testtimestamp: {
|
||||
format: 'strict_date_time',
|
||||
gte: '1901-01-01T00:00:00Z',
|
||||
lte: '1902-01-01T00:00:00Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
includes: ['testtimestamp', 'larry'],
|
||||
timezone: 'UTC',
|
||||
});
|
||||
|
||||
expect(
|
||||
getFilters(
|
||||
args.indexPatternId,
|
||||
args.indexPatternTimeField,
|
||||
args.timerange,
|
||||
args.savedSearchObjectAttr,
|
||||
undefined,
|
||||
args.queryFilter
|
||||
)
|
||||
).toEqual({
|
||||
combinedFilter: [
|
||||
{
|
||||
range: {
|
||||
testtimestamp: {
|
||||
format: 'strict_date_time',
|
||||
gte: '1901-01-01T00:00:00Z',
|
||||
lte: '1902-01-01T00:00:00Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ isFilter: true, isQueryFilter: true },
|
||||
],
|
||||
includes: ['testtimestamp', 'larry'],
|
||||
timezone: 'UTC',
|
||||
});
|
||||
});
|
||||
|
||||
describe('timefilter', () => {
|
||||
it('formats the datetime to the provided timezone', () => {
|
||||
args.timerange = {
|
||||
timezone: 'MST',
|
||||
min: '1901-01-01T00:00:00Z',
|
||||
max: '1902-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
expect(
|
||||
getFilters(
|
||||
args.indexPatternId,
|
||||
args.indexPatternTimeField,
|
||||
args.timerange,
|
||||
args.savedSearchObjectAttr
|
||||
)
|
||||
).toEqual({
|
||||
combinedFilter: [
|
||||
{
|
||||
range: {
|
||||
testtimestamp: {
|
||||
format: 'strict_date_time',
|
||||
gte: '1900-12-31T17:00:00-07:00',
|
||||
lte: '1901-12-31T17:00:00-07:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
includes: ['testtimestamp', 'larry'],
|
||||
timezone: 'MST',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { badRequest } from 'boom';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { SavedSearchObjectAttributes, TimeRangeParams } from '../../';
|
||||
import { QueryFilter, Filter, SearchSourceFilter } from './';
|
||||
|
||||
export function getFilters(
|
||||
indexPatternId: string,
|
||||
indexPatternTimeField: string | null,
|
||||
timerange: TimeRangeParams | null,
|
||||
savedSearchObjectAttr: SavedSearchObjectAttributes,
|
||||
searchSourceFilter?: SearchSourceFilter,
|
||||
queryFilter?: QueryFilter
|
||||
) {
|
||||
let includes: string[];
|
||||
let timeFilter: any | null;
|
||||
let timezone: string | null;
|
||||
|
||||
if (indexPatternTimeField) {
|
||||
if (!timerange) {
|
||||
throw badRequest(
|
||||
`Time range params are required for index pattern [${indexPatternId}], using time field [${indexPatternTimeField}]`
|
||||
);
|
||||
}
|
||||
|
||||
timezone = timerange.timezone;
|
||||
const { min: gte, max: lte } = timerange;
|
||||
timeFilter = {
|
||||
range: {
|
||||
[indexPatternTimeField]: {
|
||||
format: 'strict_date_time',
|
||||
gte: moment.tz(moment(gte), timezone).format(),
|
||||
lte: moment.tz(moment(lte), timezone).format(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const savedSearchCols = savedSearchObjectAttr.columns || [];
|
||||
includes = [indexPatternTimeField, ...savedSearchCols];
|
||||
} else {
|
||||
includes = savedSearchObjectAttr.columns || [];
|
||||
timeFilter = null;
|
||||
timezone = null;
|
||||
}
|
||||
|
||||
const combinedFilter: Filter[] = [timeFilter, searchSourceFilter, queryFilter].filter(Boolean); // builds an array of defined filters
|
||||
|
||||
return { timezone, combinedFilter, includes };
|
||||
}
|
79
x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/index.d.ts
vendored
Normal file
79
x-pack/plugins/reporting/export_types/csv_from_savedobject/server/lib/index.d.ts
vendored
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CancellationToken } from '../../../../common/cancellation_token';
|
||||
import { SavedSearchObjectAttributes, SearchPanel, SearchRequest, SearchSource } from '../../';
|
||||
|
||||
export interface SavedSearchGeneratorResult {
|
||||
content: string;
|
||||
maxSizeReached: boolean;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface CsvResultFromSearch {
|
||||
type: string;
|
||||
result: SavedSearchGeneratorResult;
|
||||
}
|
||||
|
||||
type EndpointCaller = (method: string, params: any) => Promise<any>;
|
||||
type FormatsMap = Map<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
params: {
|
||||
pattern: string;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
export interface GenerateCsvParams {
|
||||
searchRequest: SearchRequest;
|
||||
callEndpoint: EndpointCaller;
|
||||
fields: string[];
|
||||
formatsMap: FormatsMap;
|
||||
metaFields: string[]; // FIXME not sure what this is for
|
||||
conflictedTypesFields: string[]; // FIXME not sure what this is for
|
||||
cancellationToken: CancellationToken;
|
||||
settings: {
|
||||
separator: string;
|
||||
quoteValues: boolean;
|
||||
timezone: string | null;
|
||||
maxSizeBytes: number;
|
||||
scroll: { duration: string; size: number };
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* These filter types are stub types to help ensure things get passed to
|
||||
* non-Typescript functions in the right order. An actual structure is not
|
||||
* needed because the code doesn't look into the properties; just combines them
|
||||
* and passes them through to other non-TS modules.
|
||||
*/
|
||||
export interface Filter {
|
||||
isFilter: boolean;
|
||||
}
|
||||
export interface TimeFilter extends Filter {
|
||||
isTimeFilter: boolean;
|
||||
}
|
||||
export interface QueryFilter extends Filter {
|
||||
isQueryFilter: boolean;
|
||||
}
|
||||
export interface SearchSourceFilter extends Filter {
|
||||
isSearchSourceFilter: boolean;
|
||||
}
|
||||
|
||||
export interface ESQueryConfig {
|
||||
allowLeadingWildcards: boolean;
|
||||
queryStringOptions: boolean;
|
||||
ignoreFilterIfFieldNotInIndex: boolean;
|
||||
}
|
||||
|
||||
export interface IndexPatternField {
|
||||
scripted: boolean;
|
||||
lang?: string;
|
||||
script?: string;
|
||||
name: string;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './index.d';
|
||||
export { createGenerateCsv } from './generate_csv';
|
|
@ -58,7 +58,8 @@ function executeJobFn(server) {
|
|||
}))
|
||||
);
|
||||
|
||||
const stop$ = Rx.fromEventPattern(cancellationToken.on);
|
||||
const boundCancelOn = cancellationToken.on.bind(cancellationToken);
|
||||
const stop$ = Rx.fromEventPattern(boundCancelOn);
|
||||
|
||||
return process$.pipe(takeUntil(stop$)).toPromise();
|
||||
});
|
||||
|
|
|
@ -36,12 +36,17 @@ export const reporting = (kibana) => {
|
|||
'plugins/reporting/share_context_menu/register_csv_reporting',
|
||||
'plugins/reporting/share_context_menu/register_reporting',
|
||||
],
|
||||
contextMenuActions: [
|
||||
'plugins/reporting/panel_actions/get_csv_panel_action',
|
||||
],
|
||||
hacks: ['plugins/reporting/hacks/job_completion_notifier'],
|
||||
home: ['plugins/reporting/register_feature'],
|
||||
managementSections: ['plugins/reporting/views/management'],
|
||||
injectDefaultVars(server, options) { // eslint-disable-line no-unused-vars
|
||||
injectDefaultVars(server, options) {
|
||||
const config = server.config();
|
||||
return {
|
||||
reportingPollConfig: options.poll
|
||||
reportingPollConfig: options.poll,
|
||||
enablePanelActionDownload: config.get('xpack.reporting.csv.enablePanelActionDownload'),
|
||||
};
|
||||
},
|
||||
uiSettingDefaults: {
|
||||
|
@ -122,6 +127,7 @@ export const reporting = (kibana) => {
|
|||
}).default()
|
||||
}).default(),
|
||||
csv: Joi.object({
|
||||
enablePanelActionDownload: Joi.boolean().default(false),
|
||||
maxSizeBytes: Joi.number().integer().default(1024 * 1024 * 10), // bytes in a kB * kB in a mB * 10
|
||||
scroll: Joi.object({
|
||||
duration: Joi.string().regex(/^[0-9]+(d|h|m|s|ms|micros|nanos)$/, { name: 'DurationString' }).default('30s'),
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { ContextMenuAction, ContextMenuActionsRegistryProvider } from 'ui/embeddable';
|
||||
import { PanelActionAPI } from 'ui/embeddable/context_menu_actions/types';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import chrome from 'ui/chrome';
|
||||
import { API_BASE_URL_V1 } from '../../common/constants';
|
||||
|
||||
const API_BASE_URL = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`;
|
||||
|
||||
class GetCsvReportPanelAction extends ContextMenuAction {
|
||||
private isDownloading: boolean;
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: 'downloadCsvReport',
|
||||
parentPanelId: 'mainMenu',
|
||||
},
|
||||
{
|
||||
icon: 'document',
|
||||
getDisplayName: () =>
|
||||
i18n.translate('xpack.reporting.dashboard.downloadCsvPanelTitle', {
|
||||
defaultMessage: 'Download CSV',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
this.isDownloading = false;
|
||||
}
|
||||
|
||||
public async getSearchRequestBody({ searchEmbeddable }: { searchEmbeddable: any }) {
|
||||
const adapters = searchEmbeddable.getInspectorAdapters();
|
||||
if (!adapters) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (adapters.requests.requests.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return searchEmbeddable.searchScope.searchSource.getSearchRequestBody();
|
||||
}
|
||||
|
||||
public isVisible = (panelActionAPI: PanelActionAPI): boolean => {
|
||||
const enablePanelActionDownload = chrome.getInjected('enablePanelActionDownload');
|
||||
|
||||
if (!enablePanelActionDownload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { embeddable, containerState } = panelActionAPI;
|
||||
|
||||
return (
|
||||
containerState.viewMode !== 'edit' && !!embeddable && embeddable.hasOwnProperty('savedSearch')
|
||||
);
|
||||
};
|
||||
|
||||
public onClick = async (panelActionAPI: PanelActionAPI) => {
|
||||
const { embeddable } = panelActionAPI as any;
|
||||
const {
|
||||
timeRange: { from, to },
|
||||
} = embeddable;
|
||||
|
||||
if (!embeddable || this.isDownloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchEmbeddable = embeddable;
|
||||
const searchRequestBody = await this.getSearchRequestBody({ searchEmbeddable });
|
||||
const state = _.pick(searchRequestBody, ['sort', 'docvalue_fields', 'query']);
|
||||
const kibanaTimezone = chrome.getUiSettingsClient().get('dateFormat:tz');
|
||||
|
||||
const id = `search:${embeddable.savedSearch.id}`;
|
||||
const filename = embeddable.getPanelTitle();
|
||||
const timezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone;
|
||||
const fromTime = dateMath.parse(from);
|
||||
const toTime = dateMath.parse(to);
|
||||
|
||||
if (!fromTime || !toTime) {
|
||||
return this.onGenerationFail(
|
||||
new Error(`Invalid time range: From: ${fromTime}, To: ${toTime}`)
|
||||
);
|
||||
}
|
||||
|
||||
const body = JSON.stringify({
|
||||
timerange: {
|
||||
min: fromTime.format(),
|
||||
max: toTime.format(),
|
||||
timezone,
|
||||
},
|
||||
state,
|
||||
});
|
||||
|
||||
this.isDownloading = true;
|
||||
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate('xpack.reporting.dashboard.csvDownloadStartedTitle', {
|
||||
defaultMessage: `CSV Download Started`,
|
||||
}),
|
||||
text: i18n.translate('xpack.reporting.dashboard.csvDownloadStartedMessage', {
|
||||
defaultMessage: `Your CSV will download momentarily.`,
|
||||
}),
|
||||
'data-test-subj': 'csvDownloadStarted',
|
||||
});
|
||||
|
||||
await kfetch({ method: 'POST', pathname: `${API_BASE_URL}/${id}`, body })
|
||||
.then((rawResponse: string) => {
|
||||
this.isDownloading = false;
|
||||
|
||||
const download = `${filename}.csv`;
|
||||
const blob = new Blob([rawResponse], { type: 'text/csv;charset=utf-8;' });
|
||||
|
||||
// Hack for IE11 Support
|
||||
if (window.navigator.msSaveOrOpenBlob) {
|
||||
return window.navigator.msSaveOrOpenBlob(blob, download);
|
||||
}
|
||||
|
||||
const a = window.document.createElement('a');
|
||||
const downloadObject = window.URL.createObjectURL(blob);
|
||||
|
||||
a.href = downloadObject;
|
||||
a.download = download;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(downloadObject);
|
||||
document.body.removeChild(a);
|
||||
})
|
||||
.catch(this.onGenerationFail.bind(this));
|
||||
};
|
||||
|
||||
private onGenerationFail(error: Error) {
|
||||
this.isDownloading = false;
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadTitle', {
|
||||
defaultMessage: `CSV download failed`,
|
||||
}),
|
||||
text: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadMessage', {
|
||||
defaultMessage: `We couldn't generate your CSV at this time.`,
|
||||
}),
|
||||
'data-test-subj': 'downloadCsvFail',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ContextMenuActionsRegistryProvider.register(() => new GetCsvReportPanelAction());
|
|
@ -36,6 +36,7 @@ export class HeadlessChromiumDriver {
|
|||
|
||||
constructor(page: Chrome.Page, { logger, inspect }: ChromiumDriverOptions) {
|
||||
this.page = page;
|
||||
// @ts-ignore https://github.com/elastic/kibana/issues/32140
|
||||
this.logger = logger.clone(['headless-chromium-driver']);
|
||||
this.inspect = inspect;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import sinon from 'sinon';
|
||||
import { CancellationToken } from '../../helpers/cancellation_token';
|
||||
import { CancellationToken } from '../../../../../common/cancellation_token';
|
||||
|
||||
describe('CancellationToken', function () {
|
||||
let cancellationToken;
|
||||
|
@ -17,12 +17,14 @@ describe('CancellationToken', function () {
|
|||
describe('on', function () {
|
||||
[true, null, undefined, 1, 'string', {}, []].forEach(function (value) {
|
||||
it(`should throw an Error if value is ${value}`, function () {
|
||||
expect(cancellationToken.on).withArgs(value).to.throwError();
|
||||
const boundOn = cancellationToken.on.bind(cancellationToken);
|
||||
expect(boundOn).withArgs(value).to.throwError();
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts a function', function () {
|
||||
expect(cancellationToken.on).withArgs(function () {}).to.not.throwError();
|
||||
const boundOn = cancellationToken.on.bind(cancellationToken);
|
||||
expect(boundOn).withArgs(function () {}).not.to.throwError();
|
||||
});
|
||||
|
||||
it(`calls function if cancel has previously been called`, function () {
|
||||
|
@ -35,7 +37,8 @@ describe('CancellationToken', function () {
|
|||
|
||||
describe('cancel', function () {
|
||||
it('should be a function accepting no parameters', function () {
|
||||
expect(cancellationToken.cancel).withArgs().to.not.throwError();
|
||||
const boundCancel = cancellationToken.cancel.bind(cancellationToken);
|
||||
expect(boundCancel).withArgs().to.not.throwError();
|
||||
});
|
||||
|
||||
it('should call a single callback', function () {
|
||||
|
|
|
@ -9,7 +9,7 @@ import Puid from 'puid';
|
|||
import moment from 'moment';
|
||||
import { constants } from './constants';
|
||||
import { WorkerTimeoutError, UnspecifiedWorkerError } from './helpers/errors';
|
||||
import { CancellationToken } from './helpers/cancellation_token';
|
||||
import { CancellationToken } from '../../../common/cancellation_token';
|
||||
import { Poller } from '../../../../../common/poller';
|
||||
|
||||
const puid = new Puid();
|
||||
|
|
|
@ -21,7 +21,7 @@ function scan(pattern) {
|
|||
});
|
||||
}
|
||||
|
||||
const pattern = resolve(__dirname, '../../export_types/*/server/index.js');
|
||||
const pattern = resolve(__dirname, '../../export_types/*/server/index.[jt]s');
|
||||
async function exportTypesRegistryFn(server) {
|
||||
const exportTypesRegistry = new ExportTypesRegistry();
|
||||
const files = await scan(pattern);
|
||||
|
|
14
x-pack/plugins/reporting/server/lib/index.ts
Normal file
14
x-pack/plugins/reporting/server/lib/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// @ts-ignore untyped module
|
||||
export { createTaggedLogger } from './create_tagged_logger';
|
||||
// @ts-ignore untyped module
|
||||
export { cryptoFactory } from './crypto';
|
||||
// @ts-ignore untyped module
|
||||
export { oncePerServer } from './once_per_server';
|
||||
|
||||
export { LevelLogger } from './level_logger';
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Request, ResponseToolkit } from 'hapi';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { API_BASE_GENERATE_V1, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants';
|
||||
import { KbnServer } from '../../types';
|
||||
import { HandlerErrorFunction, HandlerFunction, QueuedJobPayload } from './types';
|
||||
import { getRouteOptions } from './lib/route_config_factories';
|
||||
import { getJobParamsFromRequest } from './lib/get_job_params_from_request';
|
||||
|
||||
/*
|
||||
* 1. Build `jobParams` object: job data that execution will need to reference in various parts of the lifecycle
|
||||
* 2. Pass the jobParams and other common params to `handleRoute`, a shared function to enqueue the job with the params
|
||||
* 3. Ensure that details for a queued job were returned
|
||||
*/
|
||||
const getJobFromRouteHandler = async (
|
||||
handleRoute: HandlerFunction,
|
||||
handleRouteError: HandlerErrorFunction,
|
||||
request: Request,
|
||||
h: ResponseToolkit
|
||||
): Promise<QueuedJobPayload> => {
|
||||
let result: QueuedJobPayload;
|
||||
try {
|
||||
const jobParams = getJobParamsFromRequest(request, { isImmediate: false });
|
||||
result = await handleRoute(CSV_FROM_SAVEDOBJECT_JOB_TYPE, jobParams, request, h);
|
||||
} catch (err) {
|
||||
throw handleRouteError(CSV_FROM_SAVEDOBJECT_JOB_TYPE, err);
|
||||
}
|
||||
|
||||
if (get(result, 'source.job') == null) {
|
||||
throw new Error(`The Export handler is expected to return a result with job info! ${result}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/*
|
||||
* This function registers API Endpoints for queuing Reporting jobs. The API inputs are:
|
||||
* - saved object type and ID
|
||||
* - time range and time zone
|
||||
* - application state:
|
||||
* - filters
|
||||
* - query bar
|
||||
* - local (transient) changes the user made to the saved object
|
||||
*/
|
||||
export function registerGenerateCsvFromSavedObject(
|
||||
server: KbnServer,
|
||||
handleRoute: HandlerFunction,
|
||||
handleRouteError: HandlerErrorFunction
|
||||
) {
|
||||
const routeOptions = getRouteOptions(server);
|
||||
|
||||
server.route({
|
||||
path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`,
|
||||
method: 'POST',
|
||||
options: routeOptions,
|
||||
handler: async (request: Request, h: ResponseToolkit) => {
|
||||
return getJobFromRouteHandler(handleRoute, handleRouteError, request, h);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Request, ResponseObject, ResponseToolkit } from 'hapi';
|
||||
|
||||
import { API_BASE_GENERATE_V1 } from '../../common/constants';
|
||||
import { createJobFactory, executeJobFactory } from '../../export_types/csv_from_savedobject';
|
||||
import { JobDocPayload, JobDocOutputExecuted, KbnServer } from '../../types';
|
||||
import { LevelLogger } from '../lib/level_logger';
|
||||
import { getRouteOptions } from './lib/route_config_factories';
|
||||
import { getJobParamsFromRequest } from './lib/get_job_params_from_request';
|
||||
|
||||
interface KibanaResponse extends ResponseObject {
|
||||
isBoom: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
* This function registers API Endpoints for immediate Reporting jobs. The API inputs are:
|
||||
* - saved object type and ID
|
||||
* - time range and time zone
|
||||
* - application state:
|
||||
* - filters
|
||||
* - query bar
|
||||
* - local (transient) changes the user made to the saved object
|
||||
*/
|
||||
export function registerGenerateCsvFromSavedObjectImmediate(server: KbnServer) {
|
||||
const routeOptions = getRouteOptions(server);
|
||||
|
||||
/*
|
||||
* CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does:
|
||||
* - re-use the createJob function to build up es query config
|
||||
* - re-use the executeJob function to run the scan and scroll queries and capture the entire CSV in a result object.
|
||||
*/
|
||||
server.route({
|
||||
path: `${API_BASE_GENERATE_V1}/immediate/csv/saved-object/{savedObjectType}:{savedObjectId}`,
|
||||
method: 'POST',
|
||||
options: routeOptions,
|
||||
handler: async (request: Request, h: ResponseToolkit) => {
|
||||
const logger = LevelLogger.createForServer(server, ['reporting', 'savedobject-csv']);
|
||||
const jobParams = getJobParamsFromRequest(request, { isImmediate: true });
|
||||
const createJobFn = createJobFactory(server);
|
||||
const executeJobFn = executeJobFactory(server, request);
|
||||
const jobDocPayload: JobDocPayload = await createJobFn(jobParams, request.headers, request);
|
||||
const {
|
||||
content_type: jobOutputContentType,
|
||||
content: jobOutputContent,
|
||||
size: jobOutputSize,
|
||||
}: JobDocOutputExecuted = await executeJobFn(jobDocPayload, request);
|
||||
|
||||
logger.info(`job output size: ${jobOutputSize} bytes`);
|
||||
|
||||
/*
|
||||
* ESQueue worker function defaults `content` to null, even if the
|
||||
* executeJob returned undefined.
|
||||
*
|
||||
* This converts null to undefined so the value can be sent to h.response()
|
||||
*/
|
||||
if (jobOutputContent === null) {
|
||||
logger.warn('CSV Job Execution created empty content result');
|
||||
}
|
||||
const response = h
|
||||
.response(jobOutputContent ? jobOutputContent : undefined)
|
||||
.type(jobOutputContentType);
|
||||
|
||||
// Set header for buffer download, not streaming
|
||||
const { isBoom } = response as KibanaResponse;
|
||||
if (isBoom == null) {
|
||||
response.header('accept-ranges', 'none');
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
}
|
|
@ -11,6 +11,8 @@ import { KbnServer } from '../../types';
|
|||
// @ts-ignore
|
||||
import { enqueueJobFactory } from '../lib/enqueue_job';
|
||||
import { registerGenerate } from './generate';
|
||||
import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject';
|
||||
import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate';
|
||||
import { registerJobs } from './jobs';
|
||||
import { registerLegacy } from './legacy';
|
||||
|
||||
|
@ -20,7 +22,15 @@ export function registerRoutes(server: KbnServer) {
|
|||
const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const enqueueJob = enqueueJobFactory(server);
|
||||
|
||||
async function handler(exportTypeId: any, jobParams: any, request: Request, h: ResponseToolkit) {
|
||||
/*
|
||||
* Generates enqueued job details to use in responses
|
||||
*/
|
||||
async function handler(
|
||||
exportTypeId: string,
|
||||
jobParams: any,
|
||||
request: Request,
|
||||
h: ResponseToolkit
|
||||
) {
|
||||
// @ts-ignore
|
||||
const user = request.pre.user;
|
||||
const headers = request.headers;
|
||||
|
@ -38,12 +48,12 @@ export function registerRoutes(server: KbnServer) {
|
|||
.type('application/json');
|
||||
}
|
||||
|
||||
function handleError(exportType: any, err: Error) {
|
||||
function handleError(exportTypeId: string, err: Error) {
|
||||
if (err instanceof esErrors['401']) {
|
||||
return boom.unauthorized(`Sorry, you aren't authenticated`);
|
||||
}
|
||||
if (err instanceof esErrors['403']) {
|
||||
return boom.forbidden(`Sorry, you are not authorized to create ${exportType} reports`);
|
||||
return boom.forbidden(`Sorry, you are not authorized to create ${exportTypeId} reports`);
|
||||
}
|
||||
if (err instanceof esErrors['404']) {
|
||||
return boom.boomify(err, { statusCode: 404 });
|
||||
|
@ -53,5 +63,12 @@ export function registerRoutes(server: KbnServer) {
|
|||
|
||||
registerGenerate(server, handler, handleError);
|
||||
registerLegacy(server, handler, handleError);
|
||||
|
||||
// Register beta panel-action download-related API's
|
||||
if (config.get('xpack.reporting.csv.enablePanelActionDownload')) {
|
||||
registerGenerateCsvFromSavedObject(server, handler, handleError);
|
||||
registerGenerateCsvFromSavedObjectImmediate(server);
|
||||
}
|
||||
|
||||
registerJobs(server);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Request } from 'hapi';
|
||||
import { JobParamPostPayload, JobParams } from '../../../types';
|
||||
|
||||
export function getJobParamsFromRequest(
|
||||
request: Request,
|
||||
opts: { isImmediate: boolean }
|
||||
): JobParams {
|
||||
const { savedObjectType, savedObjectId } = request.params;
|
||||
const { timerange, state } = request.payload as JobParamPostPayload;
|
||||
const post = timerange || state ? { timerange, state } : undefined;
|
||||
|
||||
return {
|
||||
isImmediate: opts.isImmediate,
|
||||
savedObjectType,
|
||||
savedObjectId,
|
||||
post,
|
||||
};
|
||||
}
|
|
@ -5,11 +5,13 @@
|
|||
*/
|
||||
|
||||
import { Request } from 'hapi';
|
||||
import Joi from 'joi';
|
||||
import { KbnServer } from '../../../types';
|
||||
// @ts-ignore
|
||||
import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing';
|
||||
// @ts-ignore
|
||||
import { reportingFeaturePreRoutingFactory } from './reporting_feature_pre_routing';
|
||||
import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants';
|
||||
|
||||
const API_TAG = 'api';
|
||||
|
||||
|
@ -41,6 +43,27 @@ export function getRouteConfigFactoryReportingPre(server: KbnServer) {
|
|||
};
|
||||
}
|
||||
|
||||
export function getRouteOptions(server: KbnServer) {
|
||||
const getRouteConfig = getRouteConfigFactoryReportingPre(server);
|
||||
return {
|
||||
...getRouteConfig(() => CSV_FROM_SAVEDOBJECT_JOB_TYPE),
|
||||
validate: {
|
||||
params: Joi.object({
|
||||
savedObjectType: Joi.string().required(),
|
||||
savedObjectId: Joi.string().required(),
|
||||
}).required(),
|
||||
payload: Joi.object({
|
||||
state: Joi.object().default({}),
|
||||
timerange: Joi.object({
|
||||
timezone: Joi.string().default('UTC'),
|
||||
min: Joi.date().required(),
|
||||
max: Joi.date().required(),
|
||||
}).optional(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getRouteConfigFactoryManagementPre(server: KbnServer) {
|
||||
const authorizedUserPreRouting = authorizedUserPreRoutingFactory(server);
|
||||
const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(server);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Request, ResponseToolkit } from 'hapi';
|
||||
import { JobDocPayload } from '../../types';
|
||||
|
||||
export type HandlerFunction = (
|
||||
exportType: any,
|
||||
|
@ -14,3 +15,12 @@ export type HandlerFunction = (
|
|||
) => any;
|
||||
|
||||
export type HandlerErrorFunction = (exportType: any, err: Error) => any;
|
||||
|
||||
export interface QueuedJobPayload {
|
||||
error?: boolean;
|
||||
source: {
|
||||
job: {
|
||||
payload: JobDocPayload;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
88
x-pack/plugins/reporting/types.d.ts
vendored
88
x-pack/plugins/reporting/types.d.ts
vendored
|
@ -18,6 +18,7 @@ export interface KbnServer {
|
|||
plugins: Record<string, any>;
|
||||
route: any;
|
||||
log: any;
|
||||
fieldFormatServiceFactory: (uiConfig: any) => any;
|
||||
savedObjects: {
|
||||
getScopedSavedObjectsClient: (
|
||||
fakeRequest: { headers: object; getBasePath: () => string }
|
||||
|
@ -28,6 +29,20 @@ export interface KbnServer {
|
|||
) => UiSettings;
|
||||
}
|
||||
|
||||
export interface ExportTypeDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
jobType: string;
|
||||
jobContentExtension: string;
|
||||
createJobFactory: () => any;
|
||||
executeJobFactory: () => any;
|
||||
validLicenses: string[];
|
||||
}
|
||||
|
||||
export interface ExportTypesRegistry {
|
||||
register: (exportTypeDefinition: ExportTypeDefinition) => void;
|
||||
}
|
||||
|
||||
export interface ConfigObject {
|
||||
get: (path?: string) => any;
|
||||
}
|
||||
|
@ -41,7 +56,7 @@ export interface Logger {
|
|||
debug: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
warning: (message: string) => void;
|
||||
clone: (tags: string[]) => Logger;
|
||||
clone?: (tags: string[]) => Logger;
|
||||
}
|
||||
|
||||
export interface ViewZoomWidthHeight {
|
||||
|
@ -92,20 +107,58 @@ export interface CryptoFactory {
|
|||
decrypt: (headers?: Record<string, string>) => string;
|
||||
}
|
||||
|
||||
export interface ReportingJob {
|
||||
headers?: Record<string, string>;
|
||||
export interface TimeRangeParams {
|
||||
timezone: string;
|
||||
min: Date | string | number;
|
||||
max: Date | string | number;
|
||||
}
|
||||
|
||||
type PostPayloadState = Partial<{
|
||||
state: {
|
||||
query: any;
|
||||
sort: any[];
|
||||
columns: string[]; // TODO
|
||||
};
|
||||
}>;
|
||||
|
||||
// retain POST payload data, needed for async
|
||||
interface JobParamPostPayload extends PostPayloadState {
|
||||
timerange: TimeRangeParams;
|
||||
}
|
||||
|
||||
// params that come into a request
|
||||
export interface JobParams {
|
||||
savedObjectType: string;
|
||||
savedObjectId: string;
|
||||
isImmediate: boolean;
|
||||
post?: JobParamPostPayload;
|
||||
panel?: any; // has to be resolved by the request handler
|
||||
visType?: string; // has to be resolved by the request handler
|
||||
}
|
||||
|
||||
export interface JobDocPayload {
|
||||
basePath?: string;
|
||||
urls?: string[];
|
||||
relativeUrl?: string;
|
||||
forceNow?: string;
|
||||
headers?: Record<string, string>;
|
||||
jobParams: JobParams;
|
||||
relativeUrl?: string;
|
||||
timeRange?: any;
|
||||
objects?: [any];
|
||||
title: string;
|
||||
urls?: string[];
|
||||
type?: string | null; // string if completed job; null if incomplete job;
|
||||
objects?: string | null; // string if completed job; null if incomplete job;
|
||||
}
|
||||
|
||||
export interface JobDocOutput {
|
||||
content: string; // encoded content
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
export interface JobDoc {
|
||||
output: any;
|
||||
jobtype: string;
|
||||
payload: ReportingJob;
|
||||
output: JobDocOutput;
|
||||
payload: JobDocPayload;
|
||||
status: string; // completed, failed, etc
|
||||
}
|
||||
|
||||
export interface JobSource {
|
||||
|
@ -113,6 +166,25 @@ export interface JobSource {
|
|||
_source: JobDoc;
|
||||
}
|
||||
|
||||
/*
|
||||
* A snake_cased field is the only significant difference in structure of
|
||||
* JobDocOutputExecuted vs JobDocOutput.
|
||||
*
|
||||
* JobDocOutput is the structure of the object returned by getDocumentPayload
|
||||
*
|
||||
* data in the _source fields of the
|
||||
* Reporting index.
|
||||
*
|
||||
* The ESQueueWorker internals have executed job objects returned with this
|
||||
* structure. See `_formatOutput` in reporting/server/lib/esqueue/worker.js
|
||||
*/
|
||||
export interface JobDocOutputExecuted {
|
||||
content_type: string; // vs `contentType` above
|
||||
content: string | null; // defaultOutput is null
|
||||
max_size_reached: boolean;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ESQueueWorker {
|
||||
on: (event: string, handler: any) => void;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ require('@kbn/plugin-helpers').babelRegister();
|
|||
require('@kbn/test').runTestsCli([
|
||||
require.resolve('../test/reporting/configs/chromium_api.js'),
|
||||
require.resolve('../test/reporting/configs/chromium_functional.js'),
|
||||
require.resolve('../test/reporting/configs/generate_api'),
|
||||
require.resolve('../test/functional/config.js'),
|
||||
require.resolve('../test/api_integration/config_security_basic.js'),
|
||||
require.resolve('../test/api_integration/config.js'),
|
||||
|
|
|
@ -21,16 +21,11 @@ import {
|
|||
SpacesServiceProvider,
|
||||
} from '../common/services';
|
||||
|
||||
export default async function ({ readConfigFile }) {
|
||||
const kibanaAPITestsConfig = await readConfigFile(
|
||||
require.resolve('../../../test/api_integration/config.js')
|
||||
);
|
||||
const xPackFunctionalTestsConfig = await readConfigFile(
|
||||
require.resolve('../functional/config.js')
|
||||
);
|
||||
const kibanaCommonConfig = await readConfigFile(
|
||||
require.resolve('../../../test/common/config.js')
|
||||
);
|
||||
export async function getApiIntegrationConfig({ readConfigFile }) {
|
||||
|
||||
const kibanaAPITestsConfig = await readConfigFile(require.resolve('../../../test/api_integration/config.js'));
|
||||
const xPackFunctionalTestsConfig = await readConfigFile(require.resolve('../functional/config.js'));
|
||||
const kibanaCommonConfig = await readConfigFile(require.resolve('../../../test/common/config.js'));
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve('./apis')],
|
||||
|
@ -73,3 +68,5 @@ export default async function ({ readConfigFile }) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default getApiIntegrationConfig;
|
||||
|
|
|
@ -192,6 +192,8 @@ export default async function ({ readConfigFile }) {
|
|||
'--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d',
|
||||
'--xpack.xpack_main.telemetry.enabled=false',
|
||||
'--xpack.maps.showMapsInspectorAdapter=true',
|
||||
'--xpack.reporting.queue.pollInterval=3000', // make it explicitly the default
|
||||
'--xpack.reporting.csv.maxSizeBytes=2850', // small-ish limit for cutting off a 1999 byte report
|
||||
'--stats.maximumWaitTimeForAllCollectorsInS=0',
|
||||
'--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions
|
||||
'--xpack.code.security.enableGitCertCheck=false', // Disable git certificate check
|
||||
|
|
BIN
x-pack/test/functional/es_archives/reporting/logs/data.json.gz
Normal file
BIN
x-pack/test/functional/es_archives/reporting/logs/data.json.gz
Normal file
Binary file not shown.
302
x-pack/test/functional/es_archives/reporting/logs/mappings.json
Normal file
302
x-pack/test/functional/es_archives/reporting/logs/mappings.json
Normal file
|
@ -0,0 +1,302 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"config": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"buildNum": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"panelsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"refreshInterval": {
|
||||
"properties": {
|
||||
"display": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"pause": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"section": {
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeFrom": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timeRestore": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"timeTo": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graph-workspace": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"numLinks": {
|
||||
"type": "integer"
|
||||
},
|
||||
"numVertices": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"wsState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index-pattern": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"fieldFormatMap": {
|
||||
"type": "text"
|
||||
},
|
||||
"fields": {
|
||||
"type": "text"
|
||||
},
|
||||
"intervalName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"notExpandable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sourceFilters": {
|
||||
"type": "text"
|
||||
},
|
||||
"timeFieldName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"space": {
|
||||
"properties": {
|
||||
"_reserved": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"color": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"initials": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 2048,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spaceId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion-sheet": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion_chart_height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_columns": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_other_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_sheet": {
|
||||
"type": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"url": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"accessCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"accessDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 2048,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualization": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"savedSearchId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"visState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_replicas": "1",
|
||||
"number_of_shards": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
x-pack/test/functional/es_archives/reporting/sales/data.json.gz
Normal file
BIN
x-pack/test/functional/es_archives/reporting/sales/data.json.gz
Normal file
Binary file not shown.
334
x-pack/test/functional/es_archives/reporting/sales/mappings.json
Normal file
334
x-pack/test/functional/es_archives/reporting/sales/mappings.json
Normal file
|
@ -0,0 +1,334 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"config": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"buildNum": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"panelsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"refreshInterval": {
|
||||
"properties": {
|
||||
"display": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"pause": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"section": {
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeFrom": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timeRestore": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"timeTo": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graph-workspace": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"numLinks": {
|
||||
"type": "integer"
|
||||
},
|
||||
"numVertices": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"wsState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index-pattern": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"fieldFormatMap": {
|
||||
"type": "text"
|
||||
},
|
||||
"fields": {
|
||||
"type": "text"
|
||||
},
|
||||
"intervalName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"notExpandable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sourceFilters": {
|
||||
"type": "text"
|
||||
},
|
||||
"timeFieldName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"space": {
|
||||
"properties": {
|
||||
"_reserved": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"color": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"initials": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 2048,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spaceId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion-sheet": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion_chart_height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_columns": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_other_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_sheet": {
|
||||
"type": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"url": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"accessCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"accessDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 2048,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualization": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"savedSearchId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"visState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_replicas": "1",
|
||||
"number_of_shards": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "sales",
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"@date": {
|
||||
"type": "date"
|
||||
},
|
||||
"metric": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"power": {
|
||||
"type": "long"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_replicas": "0",
|
||||
"number_of_shards": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,742 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"aliases": {
|
||||
".kibana": {
|
||||
}
|
||||
},
|
||||
"index": ".kibana_1",
|
||||
"mappings": {
|
||||
"_meta": {
|
||||
"migrationMappingPropertyHashes": {
|
||||
"apm-telemetry": "0383a570af33654a51c8a1352417bc6b",
|
||||
"canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231",
|
||||
"config": "87aca8fdb053154f11383fce3dbf3edf",
|
||||
"dashboard": "eb3789e1af878e73f85304333240f65f",
|
||||
"graph-workspace": "cd7ba1330e6682e9cc00b78850874be1",
|
||||
"index-pattern": "66eccb05066c5a89924f48a9e9736499",
|
||||
"infrastructure-ui-source": "10acdf67d9a06d462e198282fd6d4b81",
|
||||
"kql-telemetry": "d12a98a6f19a2d273696597547e064ee",
|
||||
"map": "23d7aa4a720d4938ccde3983f87bd58d",
|
||||
"maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d",
|
||||
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
|
||||
"ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9",
|
||||
"namespace": "2f4316de49999235636386fe51dc06c1",
|
||||
"references": "7997cf5a56cc02bdc9c93361bde732b0",
|
||||
"sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4",
|
||||
"search": "181661168bbadd1eff5902361e2a0d5c",
|
||||
"server": "ec97f1c5da1a19609a60874e5af1100c",
|
||||
"space": "0d5011d73a0ef2f0f615bb42f26f187e",
|
||||
"telemetry": "e1c8bc94e443aefd9458932cc0697a4d",
|
||||
"timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf",
|
||||
"type": "2f4316de49999235636386fe51dc06c1",
|
||||
"updated_at": "00da57df13e94e9d98437d13ace4bfe0",
|
||||
"upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6",
|
||||
"upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b",
|
||||
"url": "c7f66a0df8b1b52f17c28c4adb111105",
|
||||
"user-action": "0d409297dc5ebe1e3a1da691c6ee32e3",
|
||||
"visualization": "52d7a13ad68a150c4525b292d23e12cc"
|
||||
}
|
||||
},
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"apm-telemetry": {
|
||||
"properties": {
|
||||
"has_any_services": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"services_per_agent": {
|
||||
"properties": {
|
||||
"go": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"java": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"js-base": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"nodejs": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"python": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"ruby": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"rum-js": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"canvas-workpad": {
|
||||
"dynamic": "false",
|
||||
"properties": {
|
||||
"@created": {
|
||||
"type": "date"
|
||||
},
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"name": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"buildNum": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"dateFormat:tz": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
},
|
||||
"defaultIndex": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
},
|
||||
"search:queryLanguage": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"panelsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"refreshInterval": {
|
||||
"properties": {
|
||||
"display": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"pause": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"section": {
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeFrom": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timeRestore": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"timeTo": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graph-workspace": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"numLinks": {
|
||||
"type": "integer"
|
||||
},
|
||||
"numVertices": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"wsState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index-pattern": {
|
||||
"properties": {
|
||||
"fieldFormatMap": {
|
||||
"type": "text"
|
||||
},
|
||||
"fields": {
|
||||
"type": "text"
|
||||
},
|
||||
"intervalName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"notExpandable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sourceFilters": {
|
||||
"type": "text"
|
||||
},
|
||||
"timeFieldName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"typeMeta": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"infrastructure-ui-source": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"fields": {
|
||||
"properties": {
|
||||
"container": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"host": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"pod": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"tiebreaker": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logAlias": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"metricAlias": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kql-telemetry": {
|
||||
"properties": {
|
||||
"optInCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"optOutCount": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"properties": {
|
||||
"bounds": {
|
||||
"type": "geo_shape"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"layerListJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"mapStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"maps-telemetry": {
|
||||
"properties": {
|
||||
"attributesPerMap": {
|
||||
"properties": {
|
||||
"dataSourcesCount": {
|
||||
"properties": {
|
||||
"avg": {
|
||||
"type": "long"
|
||||
},
|
||||
"max": {
|
||||
"type": "long"
|
||||
},
|
||||
"min": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emsVectorLayersCount": {
|
||||
"dynamic": "true",
|
||||
"type": "object"
|
||||
},
|
||||
"layerTypesCount": {
|
||||
"dynamic": "true",
|
||||
"type": "object"
|
||||
},
|
||||
"layersCount": {
|
||||
"properties": {
|
||||
"avg": {
|
||||
"type": "long"
|
||||
},
|
||||
"max": {
|
||||
"type": "long"
|
||||
},
|
||||
"min": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapsTotalCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"timeCaptured": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"migrationVersion": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"dashboard": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
},
|
||||
"index-pattern": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
},
|
||||
"search": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
},
|
||||
"visualization": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ml-telemetry": {
|
||||
"properties": {
|
||||
"file_data_visualizer": {
|
||||
"properties": {
|
||||
"index_creation_count": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"references": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "nested"
|
||||
},
|
||||
"sample-data-telemetry": {
|
||||
"properties": {
|
||||
"installCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"unInstallCount": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"space": {
|
||||
"properties": {
|
||||
"_reserved": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disabledFeatures": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"color": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"disabledFeatures": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"initials": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 2048,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion-sheet": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion_chart_height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_columns": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_other_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_sheet": {
|
||||
"type": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "date"
|
||||
},
|
||||
"upgrade-assistant-reindex-operation": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"indexName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"upgrade-assistant-telemetry": {
|
||||
"properties": {
|
||||
"features": {
|
||||
"properties": {
|
||||
"deprecation_logging": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"null_value": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui_open": {
|
||||
"properties": {
|
||||
"cluster": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"indices": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"overview": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui_reindex": {
|
||||
"properties": {
|
||||
"close": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"open": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"start": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
},
|
||||
"stop": {
|
||||
"null_value": 0,
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"properties": {
|
||||
"accessCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"accessDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 2048,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user-action": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualization": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"savedSearchRefName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"visState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"index": {
|
||||
"auto_expand_replicas": "0-1",
|
||||
"number_of_replicas": "0",
|
||||
"number_of_shards": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"aliases": {
|
||||
},
|
||||
"index": "babynames",
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "date"
|
||||
},
|
||||
"gender": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"percent": {
|
||||
"type": "float"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
},
|
||||
"year": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"index": {
|
||||
"mapping": {
|
||||
"coerce": "false"
|
||||
},
|
||||
"number_of_replicas": "0",
|
||||
"number_of_shards": "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
346
x-pack/test/reporting/api/generate/csv_saved_search.ts
Normal file
346
x-pack/test/reporting/api/generate/csv_saved_search.ts
Normal file
|
@ -0,0 +1,346 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import supertest from 'supertest';
|
||||
import {
|
||||
CSV_RESULT_HUGE,
|
||||
CSV_RESULT_SCRIPTED,
|
||||
CSV_RESULT_SCRIPTED_REQUERY,
|
||||
CSV_RESULT_SCRIPTED_RESORTED,
|
||||
CSV_RESULT_TIMEBASED,
|
||||
CSV_RESULT_TIMELESS,
|
||||
} from './fixtures';
|
||||
|
||||
interface GenerateOpts {
|
||||
timerange?: {
|
||||
timezone: string;
|
||||
min: number | string | Date;
|
||||
max: number | string | Date;
|
||||
};
|
||||
state: any;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function({ getService }: { getService: any }) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertestSvc = getService('supertest');
|
||||
const generateAPI = {
|
||||
getCsvFromSavedSearch: async (
|
||||
id: string,
|
||||
{ timerange, state }: GenerateOpts,
|
||||
isImmediate = true
|
||||
) => {
|
||||
return await supertestSvc
|
||||
.post(`/api/reporting/v1/generate/${isImmediate ? 'immediate/' : ''}csv/saved-object/${id}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({ timerange, state });
|
||||
},
|
||||
};
|
||||
|
||||
describe('Generation from Saved Search ID', () => {
|
||||
describe('Saved Search Features', () => {
|
||||
it('With filters and timebased data', async () => {
|
||||
// load test data that contains a saved search and documents
|
||||
await esArchiver.load('reporting/logs');
|
||||
await esArchiver.load('logstash_functional');
|
||||
|
||||
// TODO: check headers for inline filename
|
||||
const {
|
||||
status: resStatus,
|
||||
text: resText,
|
||||
type: resType,
|
||||
} = (await generateAPI.getCsvFromSavedSearch(
|
||||
'search:d7a79750-3edd-11e9-99cc-4d80163ee9e7',
|
||||
{
|
||||
timerange: {
|
||||
timezone: 'UTC',
|
||||
min: '2015-09-19T10:00:00.000Z',
|
||||
max: '2015-09-21T10:00:00.000Z',
|
||||
},
|
||||
state: {},
|
||||
}
|
||||
)) as supertest.Response;
|
||||
|
||||
expect(resStatus).to.eql(200);
|
||||
expect(resType).to.eql('text/csv');
|
||||
expect(resText).to.eql(CSV_RESULT_TIMEBASED);
|
||||
|
||||
await esArchiver.unload('reporting/logs');
|
||||
await esArchiver.unload('logstash_functional');
|
||||
});
|
||||
|
||||
it('With filters and non-timebased data', async () => {
|
||||
// load test data that contains a saved search and documents
|
||||
await esArchiver.load('reporting/sales');
|
||||
|
||||
const {
|
||||
status: resStatus,
|
||||
text: resText,
|
||||
type: resType,
|
||||
} = (await generateAPI.getCsvFromSavedSearch(
|
||||
'search:71e3ee20-3f99-11e9-b8ee-6b9604f2f877',
|
||||
{
|
||||
state: {},
|
||||
}
|
||||
)) as supertest.Response;
|
||||
|
||||
expect(resStatus).to.eql(200);
|
||||
expect(resType).to.eql('text/csv');
|
||||
expect(resText).to.eql(CSV_RESULT_TIMELESS);
|
||||
|
||||
await esArchiver.unload('reporting/sales');
|
||||
});
|
||||
|
||||
it('With scripted fields and field formatters', async () => {
|
||||
// load test data that contains a saved search and documents
|
||||
await esArchiver.load('reporting/scripted');
|
||||
|
||||
const {
|
||||
status: resStatus,
|
||||
text: resText,
|
||||
type: resType,
|
||||
} = (await generateAPI.getCsvFromSavedSearch(
|
||||
'search:f34bf440-5014-11e9-bce7-4dabcb8bef24',
|
||||
{
|
||||
timerange: {
|
||||
timezone: 'UTC',
|
||||
min: '1979-01-01T10:00:00Z',
|
||||
max: '1981-01-01T10:00:00Z',
|
||||
},
|
||||
state: {},
|
||||
}
|
||||
)) as supertest.Response;
|
||||
|
||||
expect(resStatus).to.eql(200);
|
||||
expect(resType).to.eql('text/csv');
|
||||
expect(resText).to.eql(CSV_RESULT_SCRIPTED);
|
||||
|
||||
await esArchiver.unload('reporting/scripted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Features', () => {
|
||||
it('Return a 404', async () => {
|
||||
const { body } = (await generateAPI.getCsvFromSavedSearch('search:gobbledygook', {
|
||||
timerange: { timezone: 'UTC', min: 63097200000, max: 126255599999 },
|
||||
state: {},
|
||||
})) as supertest.Response;
|
||||
const expectedBody = {
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [search/gobbledygook] not found',
|
||||
statusCode: 404,
|
||||
};
|
||||
expect(body).to.eql(expectedBody);
|
||||
});
|
||||
|
||||
it('Return 400 if time range param is needed but missing', async () => {
|
||||
// load test data that contains a saved search and documents
|
||||
await esArchiver.load('reporting/logs');
|
||||
await esArchiver.load('logstash_functional');
|
||||
|
||||
const {
|
||||
status: resStatus,
|
||||
text: resText,
|
||||
type: resType,
|
||||
} = (await generateAPI.getCsvFromSavedSearch(
|
||||
'search:d7a79750-3edd-11e9-99cc-4d80163ee9e7',
|
||||
{ state: {} }
|
||||
)) as supertest.Response;
|
||||
|
||||
expect(resStatus).to.eql(400);
|
||||
expect(resType).to.eql('application/json');
|
||||
const { message: errorMessage } = JSON.parse(resText);
|
||||
expect(errorMessage).to.eql(
|
||||
'Time range params are required for index pattern [logstash-*], using time field [@timestamp]'
|
||||
);
|
||||
|
||||
await esArchiver.unload('reporting/logs');
|
||||
await esArchiver.unload('logstash_functional');
|
||||
});
|
||||
|
||||
it('Stops at Max Size Reached', async () => {
|
||||
// load test data that contains a saved search and documents
|
||||
await esArchiver.load('reporting/scripted');
|
||||
|
||||
const {
|
||||
status: resStatus,
|
||||
text: resText,
|
||||
type: resType,
|
||||
} = (await generateAPI.getCsvFromSavedSearch(
|
||||
'search:f34bf440-5014-11e9-bce7-4dabcb8bef24',
|
||||
{
|
||||
timerange: {
|
||||
timezone: 'UTC',
|
||||
min: '1960-01-01T10:00:00Z',
|
||||
max: '1999-01-01T10:00:00Z',
|
||||
},
|
||||
state: {},
|
||||
}
|
||||
)) as supertest.Response;
|
||||
|
||||
expect(resStatus).to.eql(200);
|
||||
expect(resType).to.eql('text/csv');
|
||||
expect(resText).to.eql(CSV_RESULT_HUGE);
|
||||
|
||||
await esArchiver.unload('reporting/scripted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Merge user state into the query', () => {
|
||||
it('for query', async () => {
|
||||
// load test data that contains a saved search and documents
|
||||
await esArchiver.load('reporting/scripted');
|
||||
|
||||
const params = {
|
||||
searchId: 'search:f34bf440-5014-11e9-bce7-4dabcb8bef24',
|
||||
postPayload: {
|
||||
timerange: { timezone: 'UTC', min: '1979-01-01T10:00:00Z', max: '1981-01-01T10:00:00Z' }, // prettier-ignore
|
||||
state: { query: { bool: { filter: [ { bool: { filter: [ { bool: { minimum_should_match: 1, should: [{ query_string: { fields: ['name'], query: 'Fe*' } }] } } ] } } ] } } } // prettier-ignore
|
||||
},
|
||||
isImmediate: true,
|
||||
};
|
||||
const {
|
||||
status: resStatus,
|
||||
text: resText,
|
||||
type: resType,
|
||||
} = (await generateAPI.getCsvFromSavedSearch(
|
||||
params.searchId,
|
||||
params.postPayload,
|
||||
params.isImmediate
|
||||
)) as supertest.Response;
|
||||
|
||||
expect(resStatus).to.eql(200);
|
||||
expect(resType).to.eql('text/csv');
|
||||
expect(resText).to.eql(CSV_RESULT_SCRIPTED_REQUERY);
|
||||
|
||||
await esArchiver.unload('reporting/scripted');
|
||||
});
|
||||
|
||||
it('for sort', async () => {
|
||||
// load test data that contains a saved search and documents
|
||||
await esArchiver.load('reporting/scripted');
|
||||
|
||||
const {
|
||||
status: resStatus,
|
||||
text: resText,
|
||||
type: resType,
|
||||
} = (await generateAPI.getCsvFromSavedSearch(
|
||||
'search:f34bf440-5014-11e9-bce7-4dabcb8bef24',
|
||||
{
|
||||
timerange: {
|
||||
timezone: 'UTC',
|
||||
min: '1979-01-01T10:00:00Z',
|
||||
max: '1981-01-01T10:00:00Z',
|
||||
},
|
||||
state: { sort: [{ name: { order: 'asc', unmapped_type: 'boolean' } }] },
|
||||
}
|
||||
)) as supertest.Response;
|
||||
|
||||
expect(resStatus).to.eql(200);
|
||||
expect(resType).to.eql('text/csv');
|
||||
expect(resText).to.eql(CSV_RESULT_SCRIPTED_RESORTED);
|
||||
|
||||
await esArchiver.unload('reporting/scripted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Non-Immediate', () => {
|
||||
it('using queries in job params', async () => {
|
||||
// load test data that contains a saved search and documents
|
||||
await esArchiver.load('reporting/scripted');
|
||||
|
||||
const params = {
|
||||
searchId: 'search:f34bf440-5014-11e9-bce7-4dabcb8bef24',
|
||||
postPayload: {
|
||||
timerange: { timezone: 'UTC', min: '1979-01-01T10:00:00Z', max: '1981-01-01T10:00:00Z' }, // prettier-ignore
|
||||
state: { query: { bool: { filter: [ { bool: { filter: [ { bool: { minimum_should_match: 1, should: [{ query_string: { fields: ['name'], query: 'Fe*' } }] } } ] } } ] } } } // prettier-ignore
|
||||
},
|
||||
isImmediate: false,
|
||||
};
|
||||
const {
|
||||
status: resStatus,
|
||||
text: resText,
|
||||
type: resType,
|
||||
} = (await generateAPI.getCsvFromSavedSearch(
|
||||
params.searchId,
|
||||
params.postPayload,
|
||||
params.isImmediate
|
||||
)) as supertest.Response;
|
||||
|
||||
expect(resStatus).to.eql(200);
|
||||
expect(resType).to.eql('application/json');
|
||||
const {
|
||||
path: jobDownloadPath,
|
||||
job: { index: jobIndex, jobtype: jobType, created_by: jobCreatedBy, payload: jobPayload },
|
||||
} = JSON.parse(resText);
|
||||
|
||||
expect(jobDownloadPath.slice(0, 29)).to.equal('/api/reporting/jobs/download/');
|
||||
expect(jobIndex.slice(0, 11)).to.equal('.reporting-');
|
||||
expect(jobType).to.be('csv_from_savedobject');
|
||||
expect(jobCreatedBy).to.be('elastic');
|
||||
|
||||
const {
|
||||
title: payloadTitle,
|
||||
objects: payloadObjects,
|
||||
jobParams: payloadParams,
|
||||
} = jobPayload;
|
||||
expect(payloadTitle).to.be('EVERYBABY2');
|
||||
expect(payloadObjects).to.be(null); // value for non-immediate
|
||||
expect(payloadParams.savedObjectType).to.be('search');
|
||||
expect(payloadParams.savedObjectId).to.be('f34bf440-5014-11e9-bce7-4dabcb8bef24');
|
||||
expect(payloadParams.isImmediate).to.be(false);
|
||||
|
||||
const { state: postParamState, timerange: postParamTimerange } = payloadParams.post;
|
||||
expect(postParamState).to.eql({
|
||||
query: { bool: { filter: [ { bool: { filter: [ { bool: { minimum_should_match: 1, should: [{ query_string: { fields: ['name'], query: 'Fe*' } }] } } ] } } ] } } // prettier-ignore
|
||||
});
|
||||
expect(postParamTimerange).to.eql({
|
||||
max: '1981-01-01T10:00:00.000Z',
|
||||
min: '1979-01-01T10:00:00.000Z',
|
||||
timezone: 'UTC',
|
||||
});
|
||||
|
||||
const {
|
||||
indexPatternSavedObjectId: payloadPanelIndexPatternSavedObjectId,
|
||||
timerange: payloadPanelTimerange,
|
||||
} = payloadParams.panel;
|
||||
expect(payloadPanelIndexPatternSavedObjectId).to.be('89655130-5013-11e9-bce7-4dabcb8bef24');
|
||||
expect(payloadPanelTimerange).to.eql({
|
||||
timezone: 'UTC',
|
||||
min: '1979-01-01T10:00:00.000Z',
|
||||
max: '1981-01-01T10:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(payloadParams.visType).to.be('search');
|
||||
|
||||
// check the resource at jobDownloadPath
|
||||
const downloadFromPath = async (downloadPath: string) => {
|
||||
const { status, text, type } = await supertestSvc
|
||||
.get(downloadPath)
|
||||
.set('kbn-xsrf', 'xxx');
|
||||
return {
|
||||
status,
|
||||
text,
|
||||
type,
|
||||
};
|
||||
};
|
||||
|
||||
await new Promise(resolve => {
|
||||
setTimeout(async () => {
|
||||
const { status, text, type } = await downloadFromPath(jobDownloadPath);
|
||||
expect(status).to.eql(200);
|
||||
expect(type).to.eql('text/csv');
|
||||
expect(text).to.eql(CSV_RESULT_SCRIPTED_REQUERY);
|
||||
resolve();
|
||||
}, 5000); // x-pack/test/functional/config settings are inherited, uses 3 seconds for polling interval.
|
||||
});
|
||||
|
||||
await esArchiver.unload('reporting/scripted');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
175
x-pack/test/reporting/api/generate/fixtures.ts
Normal file
175
x-pack/test/reporting/api/generate/fixtures.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const CSV_RESULT_TIMEBASED = `"@timestamp",clientip,extension
|
||||
"2015-09-20T10:26:48.725Z","74.214.76.90",jpg
|
||||
"2015-09-20T10:26:48.540Z","146.86.123.109",jpg
|
||||
"2015-09-20T10:26:48.353Z","233.126.159.144",jpg
|
||||
"2015-09-20T10:26:45.468Z","153.139.156.196",png
|
||||
"2015-09-20T10:26:34.063Z","25.140.171.133",css
|
||||
"2015-09-20T10:26:11.181Z","239.249.202.59",jpg
|
||||
"2015-09-20T10:26:00.639Z","95.59.225.31",css
|
||||
"2015-09-20T10:26:00.094Z","247.174.57.245",jpg
|
||||
"2015-09-20T10:25:55.744Z","116.126.47.226",css
|
||||
"2015-09-20T10:25:54.701Z","169.228.188.120",jpg
|
||||
"2015-09-20T10:25:52.360Z","74.224.77.232",css
|
||||
"2015-09-20T10:25:49.913Z","97.83.96.39",css
|
||||
"2015-09-20T10:25:44.979Z","175.188.44.145",css
|
||||
"2015-09-20T10:25:40.968Z","89.143.125.181",jpg
|
||||
"2015-09-20T10:25:36.331Z","231.169.195.137",css
|
||||
"2015-09-20T10:25:34.064Z","137.205.146.206",jpg
|
||||
"2015-09-20T10:25:32.312Z","53.0.188.251",jpg
|
||||
"2015-09-20T10:25:27.254Z","111.214.104.239",jpg
|
||||
"2015-09-20T10:25:22.561Z","111.46.85.146",jpg
|
||||
"2015-09-20T10:25:06.674Z","55.100.60.111",jpg
|
||||
"2015-09-20T10:25:05.114Z","34.197.178.155",jpg
|
||||
"2015-09-20T10:24:55.114Z","163.123.136.118",jpg
|
||||
"2015-09-20T10:24:54.818Z","11.195.163.57",jpg
|
||||
"2015-09-20T10:24:53.742Z","96.222.137.213",png
|
||||
"2015-09-20T10:24:48.798Z","227.228.214.218",jpg
|
||||
"2015-09-20T10:24:20.223Z","228.53.110.116",jpg
|
||||
"2015-09-20T10:24:01.794Z","196.131.253.111",png
|
||||
"2015-09-20T10:23:49.521Z","125.163.133.47",jpg
|
||||
"2015-09-20T10:23:45.816Z","148.47.216.255",jpg
|
||||
"2015-09-20T10:23:36.052Z","51.105.100.214",jpg
|
||||
"2015-09-20T10:23:34.323Z","41.210.252.157",gif
|
||||
"2015-09-20T10:23:27.213Z","248.163.75.193",png
|
||||
"2015-09-20T10:23:14.866Z","48.43.210.167",png
|
||||
"2015-09-20T10:23:10.578Z","33.95.78.209",css
|
||||
"2015-09-20T10:23:07.001Z","96.40.73.208",css
|
||||
"2015-09-20T10:23:02.876Z","174.32.230.63",jpg
|
||||
"2015-09-20T10:23:00.019Z","140.233.207.177",jpg
|
||||
"2015-09-20T10:22:47.447Z","37.127.124.65",jpg
|
||||
"2015-09-20T10:22:45.803Z","130.171.208.139",png
|
||||
"2015-09-20T10:22:45.590Z","39.250.210.253",jpg
|
||||
"2015-09-20T10:22:43.997Z","248.239.221.43",css
|
||||
"2015-09-20T10:22:36.107Z","232.64.207.109",gif
|
||||
"2015-09-20T10:22:30.527Z","24.186.122.118",jpg
|
||||
"2015-09-20T10:22:25.697Z","23.3.174.206",jpg
|
||||
"2015-09-20T10:22:08.272Z","185.170.80.142",php
|
||||
"2015-09-20T10:21:40.822Z","202.22.74.232",png
|
||||
"2015-09-20T10:21:36.210Z","39.227.27.167",jpg
|
||||
"2015-09-20T10:21:19.154Z","140.233.207.177",jpg
|
||||
"2015-09-20T10:21:09.852Z","22.151.97.227",jpg
|
||||
"2015-09-20T10:21:06.079Z","157.39.25.197",css
|
||||
"2015-09-20T10:21:01.357Z","37.127.124.65",jpg
|
||||
"2015-09-20T10:20:56.519Z","23.184.94.58",jpg
|
||||
"2015-09-20T10:20:40.189Z","80.83.92.252",jpg
|
||||
"2015-09-20T10:20:27.012Z","66.194.157.171",png
|
||||
"2015-09-20T10:20:24.450Z","15.191.218.38",jpg
|
||||
"2015-09-20T10:19:45.764Z","199.113.69.162",jpg
|
||||
"2015-09-20T10:19:43.754Z","171.243.18.67",gif
|
||||
"2015-09-20T10:19:41.208Z","126.87.234.213",jpg
|
||||
"2015-09-20T10:19:40.307Z","78.216.173.242",css
|
||||
`;
|
||||
|
||||
export const CSV_RESULT_TIMELESS = `name,power
|
||||
"Jonelle-Jane Marth","1.1768"
|
||||
"Suzie-May Rishel","1.824"
|
||||
"Suzie-May Rishel","2.077"
|
||||
"Rosana Casto","2.8084"
|
||||
"Stephen Cortez","4.9856"
|
||||
"Jonelle-Jane Marth","6.156"
|
||||
"Jonelle-Jane Marth","7.0966"
|
||||
"Florinda Alejandro","10.3734"
|
||||
"Jonelle-Jane Marth","14.8074"
|
||||
"Suzie-May Rishel","19.7377"
|
||||
"Suzie-May Rishel","20.9198"
|
||||
"Florinda Alejandro","22.2092"
|
||||
`;
|
||||
|
||||
export const CSV_RESULT_SCRIPTED = `date,year,name,value,"years_ago"
|
||||
"1981-01-01T00:00:00.000Z",1981,Fetty,1763,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Fonnie,2330,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Farbara,6456,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Felinda,1886,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Frenda,7162,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Feth,3685,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Feverly,1987,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Fecky,1930,38
|
||||
"1980-01-01T00:00:00.000Z",1980,Fonnie,2748,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Frenda,8335,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Fetty,1967,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Farbara,8026,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Feth,4246,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Feverly,2249,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Fecky,2071,39
|
||||
`;
|
||||
|
||||
export const CSV_RESULT_SCRIPTED_REQUERY = `date,year,name,value,"years_ago"
|
||||
"1981-01-01T00:00:00.000Z",1981,Fetty,1763,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Felinda,1886,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Feth,3685,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Feverly,1987,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Fecky,1930,38
|
||||
"1980-01-01T00:00:00.000Z",1980,Fetty,1967,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Feth,4246,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Feverly,2249,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Fecky,2071,39
|
||||
`;
|
||||
|
||||
export const CSV_RESULT_SCRIPTED_RESORTED = `date,year,name,value,"years_ago"
|
||||
"1981-01-01T00:00:00.000Z",1981,Farbara,6456,38
|
||||
"1980-01-01T00:00:00.000Z",1980,Farbara,8026,39
|
||||
"1981-01-01T00:00:00.000Z",1981,Fecky,1930,38
|
||||
"1980-01-01T00:00:00.000Z",1980,Fecky,2071,39
|
||||
"1981-01-01T00:00:00.000Z",1981,Felinda,1886,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Feth,3685,38
|
||||
"1980-01-01T00:00:00.000Z",1980,Feth,4246,39
|
||||
"1981-01-01T00:00:00.000Z",1981,Fetty,1763,38
|
||||
"1980-01-01T00:00:00.000Z",1980,Fetty,1967,39
|
||||
"1981-01-01T00:00:00.000Z",1981,Feverly,1987,38
|
||||
"1980-01-01T00:00:00.000Z",1980,Feverly,2249,39
|
||||
"1981-01-01T00:00:00.000Z",1981,Fonnie,2330,38
|
||||
"1980-01-01T00:00:00.000Z",1980,Fonnie,2748,39
|
||||
"1981-01-01T00:00:00.000Z",1981,Frenda,7162,38
|
||||
"1980-01-01T00:00:00.000Z",1980,Frenda,8335,39
|
||||
`;
|
||||
|
||||
export const CSV_RESULT_HUGE = `date,year,name,value,"years_ago"
|
||||
"1984-01-01T00:00:00.000Z",1984,Fobby,2791,35
|
||||
"1984-01-01T00:00:00.000Z",1984,Frent,3416,35
|
||||
"1984-01-01T00:00:00.000Z",1984,Frett,2679,35
|
||||
"1984-01-01T00:00:00.000Z",1984,Filly,3366,35
|
||||
"1984-01-01T00:00:00.000Z",1984,Frian,34468,35
|
||||
"1984-01-01T00:00:00.000Z",1984,Fenjamin,7191,35
|
||||
"1984-01-01T00:00:00.000Z",1984,Frandon,5863,35
|
||||
"1984-01-01T00:00:00.000Z",1984,Fruce,1855,35
|
||||
"1984-01-01T00:00:00.000Z",1984,Fryan,7236,35
|
||||
"1984-01-01T00:00:00.000Z",1984,Frad,2482,35
|
||||
"1984-01-01T00:00:00.000Z",1984,Fradley,5175,35
|
||||
"1983-01-01T00:00:00.000Z",1983,Fryan,7114,36
|
||||
"1983-01-01T00:00:00.000Z",1983,Fradley,4752,36
|
||||
"1983-01-01T00:00:00.000Z",1983,Frian,35717,36
|
||||
"1983-01-01T00:00:00.000Z",1983,Farbara,4434,36
|
||||
"1983-01-01T00:00:00.000Z",1983,Fenjamin,5235,36
|
||||
"1983-01-01T00:00:00.000Z",1983,Fruce,1914,36
|
||||
"1983-01-01T00:00:00.000Z",1983,Fobby,2888,36
|
||||
"1983-01-01T00:00:00.000Z",1983,Frett,3031,36
|
||||
"1982-01-01T00:00:00.000Z",1982,Fonnie,1853,37
|
||||
"1982-01-01T00:00:00.000Z",1982,Frandy,2082,37
|
||||
"1982-01-01T00:00:00.000Z",1982,Fecky,1786,37
|
||||
"1982-01-01T00:00:00.000Z",1982,Frandi,2056,37
|
||||
"1982-01-01T00:00:00.000Z",1982,Fridget,1864,37
|
||||
"1982-01-01T00:00:00.000Z",1982,Farbara,5081,37
|
||||
"1982-01-01T00:00:00.000Z",1982,Feth,2818,37
|
||||
"1982-01-01T00:00:00.000Z",1982,Frenda,6270,37
|
||||
"1981-01-01T00:00:00.000Z",1981,Fetty,1763,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Fonnie,2330,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Farbara,6456,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Felinda,1886,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Frenda,7162,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Feth,3685,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Feverly,1987,38
|
||||
"1981-01-01T00:00:00.000Z",1981,Fecky,1930,38
|
||||
"1980-01-01T00:00:00.000Z",1980,Fonnie,2748,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Frenda,8335,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Fetty,1967,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Farbara,8026,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Feth,4246,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Feverly,2249,39
|
||||
"1980-01-01T00:00:00.000Z",1980,Fecky,2071,39
|
||||
`;
|
12
x-pack/test/reporting/api/generate/index.js
Normal file
12
x-pack/test/reporting/api/generate/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export default function ({ loadTestFile }) {
|
||||
describe('CSV', function () {
|
||||
this.tags('ciGroup2');
|
||||
loadTestFile(require.resolve('./csv_saved_search'));
|
||||
});
|
||||
}
|
|
@ -24,14 +24,18 @@ export async function getReportingApiConfig({ readConfigFile }) {
|
|||
junit: {
|
||||
reportName: 'X-Pack Reporting API Tests',
|
||||
},
|
||||
testFiles: [require.resolve('../api/generate')],
|
||||
esTestCluster: apiConfig.get('esTestCluster'),
|
||||
kbnTestServer: {
|
||||
...apiConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...apiConfig.get('kbnTestServer.serverArgs'),
|
||||
'--xpack.reporting.csv.enablePanelActionDownload=true',
|
||||
`--optimize.enabled=true`,
|
||||
'--logging.events.log', JSON.stringify(['info', 'warning', 'error', 'fatal', 'optimize', 'reporting'])
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default getReportingApiConfig;
|
||||
|
|
|
@ -20,6 +20,7 @@ export default async function ({ readConfigFile }) {
|
|||
...reportingApiConfig.kbnTestServer,
|
||||
serverArgs: [
|
||||
...reportingApiConfig.kbnTestServer.serverArgs,
|
||||
'--xpack.reporting.csv.enablePanelActionDownload=true',
|
||||
`--xpack.reporting.capture.browser.type=chromium`,
|
||||
`--xpack.spaces.enabled=false`,
|
||||
],
|
||||
|
|
|
@ -20,6 +20,7 @@ export default async function ({ readConfigFile }) {
|
|||
...functionalConfig.kbnTestServer,
|
||||
serverArgs: [
|
||||
...functionalConfig.kbnTestServer.serverArgs,
|
||||
'--xpack.reporting.csv.enablePanelActionDownload=true',
|
||||
`--xpack.reporting.capture.browser.type=chromium`,
|
||||
],
|
||||
},
|
||||
|
|
|
@ -25,6 +25,7 @@ export async function getFunctionalConfig({ readConfigFile }) {
|
|||
...xPackFunctionalTestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--xpack.reporting.csv.enablePanelActionDownload=true',
|
||||
'--logging.events.log', JSON.stringify(['info', 'warning', 'error', 'fatal', 'optimize', 'reporting'])
|
||||
],
|
||||
},
|
||||
|
|
32
x-pack/test/reporting/configs/generate_api.js
Normal file
32
x-pack/test/reporting/configs/generate_api.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getApiIntegrationConfig } from '../../api_integration/config';
|
||||
import { getReportingApiConfig } from './api';
|
||||
|
||||
export default async function ({ readConfigFile }) {
|
||||
const apiTestConfig = await getApiIntegrationConfig({ readConfigFile });
|
||||
const reportingApiConfig = await getReportingApiConfig({ readConfigFile });
|
||||
const xPackFunctionalTestsConfig = await readConfigFile(require.resolve('../../functional/config.js'));
|
||||
|
||||
return {
|
||||
...reportingApiConfig,
|
||||
junit: { reportName: 'X-Pack Reporting Generate API Integration Tests' },
|
||||
testFiles: [require.resolve('../api/generate')],
|
||||
services: {
|
||||
...apiTestConfig.services,
|
||||
...reportingApiConfig.services,
|
||||
},
|
||||
kbnTestServer: {
|
||||
...xPackFunctionalTestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--xpack.reporting.csv.enablePanelActionDownload=true',
|
||||
],
|
||||
},
|
||||
esArchiver: apiTestConfig.esArchiver,
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue