[Reporting] Clean Up TypeScript Definitions (#76566)

* [Reporting] Simplify Export Type Definitions, use defaults for generics, refactor

* ReportApiJSON interface for common

* rename JobSummary to JobStatusBucket for clarity

* revert unneeded create mock changes

* clean up the diff

* revert changes to worker.js

* rewrite comment

* rename type to jobtype in JobStatusBucket

* allow type inference

* JobSummarySet

* remove odd comment

* Reflect that browser timezone may be undefined in the BaseParams

* comment about optional browserTimezone

* revert unecessary es archive change

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Tim Sullivan 2020-09-22 12:33:59 -07:00 committed by GitHub
parent 42026cbbf5
commit a537f9af50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 699 additions and 894 deletions

View file

@ -7,7 +7,12 @@
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
export { ReportingConfigType } from '../server/config';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
export { LayoutInstance } from '../server/lib/layouts';
import { LayoutParams } from '../server/lib/layouts';
export { LayoutParams };
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
export { ReportDocument, ReportSource } from '../server/lib/store/report';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
export { BaseParams } from '../server/types';
export type JobId = string;
export type JobStatus =
@ -17,45 +22,43 @@ export type JobStatus =
| 'processing'
| 'failed';
export interface SourceJob {
_id: JobId;
_source: {
status: JobStatus;
output: {
max_size_reached: boolean;
csv_contains_formulas: boolean;
};
payload: {
type: string;
title: string;
};
};
}
export interface JobContent {
content: string;
}
export interface JobSummary {
id: JobId;
status: JobStatus;
title: string;
type: string;
maxSizeReached: boolean;
csvContainsFormulas: boolean;
export interface ReportApiJSON {
id: string;
index: string;
kibana_name: string;
kibana_id: string;
browser_type: string | undefined;
created_at: string;
priority?: number;
jobtype: string;
created_by: string | false;
timeout?: number;
output?: {
content_type: string;
size: number;
warnings?: string[];
};
process_expiration?: string;
completed_at: string | undefined;
payload: {
layout?: LayoutParams;
title: string;
browserTimezone?: string;
};
meta: {
layout?: string;
objectType: string;
};
max_attempts: number;
started_at: string | undefined;
attempts: number;
status: string;
}
export interface JobStatusBuckets {
completed: JobSummary[];
failed: JobSummary[];
}
type DownloadLink = string;
export type DownloadReportFn = (jobId: JobId) => DownloadLink;
type ManagementLink = string;
export type ManagementLinkFn = () => ManagementLink;
export interface PollerOptions {
functionToPoll: () => Promise<any>;
pollFrequencyInMillis: number;

View file

@ -15,10 +15,11 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import React, { Component, Fragment } from 'react';
import { get } from 'lodash';
import React, { Component, Fragment } from 'react';
import { ReportApiJSON } from '../../../common/types';
import { USES_HEADLESS_JOB_TYPES } from '../../../constants';
import { JobInfo, ReportingAPIClient } from '../../lib/reporting_api_client';
import { ReportingAPIClient } from '../../lib/reporting_api_client';
interface Props {
jobId: string;
@ -29,14 +30,14 @@ interface State {
isLoading: boolean;
isFlyoutVisible: boolean;
calloutTitle: string;
info: JobInfo | null;
info: ReportApiJSON | null;
error: Error | null;
}
const NA = 'n/a';
const UNKNOWN = 'unknown';
const getDimensions = (info: JobInfo): string => {
const getDimensions = (info: ReportApiJSON): string => {
const defaultDimensions = { width: null, height: null };
const { width, height } = get(info, 'payload.layout.dimensions', defaultDimensions);
if (width && height) {
@ -121,10 +122,6 @@ export class ReportInfoButton extends Component<Props, State> {
title: 'Title',
description: get(info, 'payload.title') || NA,
},
{
title: 'Type',
description: get(info, 'payload.type') || NA,
},
{
title: 'Layout',
description: get(info, 'meta.layout') || NA,
@ -263,7 +260,7 @@ export class ReportInfoButton extends Component<Props, State> {
private loadInfo = async () => {
this.setState({ isLoading: true });
try {
const info: JobInfo = await this.props.apiClient.getInfo(this.props.jobId);
const info: ReportApiJSON = await this.props.apiClient.getInfo(this.props.jobId);
if (this.mounted) {
this.setState({ isLoading: false, info });
}

View file

@ -7,7 +7,8 @@
import { EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { JobId, JobSummary } from '../../common/types';
import { JobSummary } from '../';
import { JobId } from '../../common/types';
interface Props {
getUrl: (jobId: JobId) => string;

View file

@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment } from 'react';
import { ToastInput } from 'src/core/public';
import { JobSummary, ManagementLinkFn } from '../';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobSummary, ManagementLinkFn } from '../../common/types';
export const getFailureToast = (
errorText: string,
@ -22,7 +22,7 @@ export const getFailureToast = (
<FormattedMessage
id="xpack.reporting.publicNotifier.error.couldNotCreateReportTitle"
defaultMessage="Could not create report for {reportObjectType} '{reportObjectTitle}'."
values={{ reportObjectType: job.type, reportObjectTitle: job.title }}
values={{ reportObjectType: job.jobtype, reportObjectTitle: job.title }}
/>
),
text: toMountPoint(

View file

@ -7,8 +7,9 @@
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment } from 'react';
import { ToastInput } from 'src/core/public';
import { JobSummary } from '../';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobId, JobSummary } from '../../common/types';
import { JobId } from '../../common/types';
import { DownloadButton } from './job_download_button';
import { ReportLink } from './report_link';
@ -21,7 +22,7 @@ export const getSuccessToast = (
<FormattedMessage
id="xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle"
defaultMessage="Created report for {reportObjectType} '{reportObjectTitle}'"
values={{ reportObjectType: job.type, reportObjectTitle: job.title }}
values={{ reportObjectType: job.jobtype, reportObjectTitle: job.title }}
/>
),
color: 'success',

View file

@ -7,8 +7,9 @@
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment } from 'react';
import { ToastInput } from 'src/core/public';
import { JobSummary } from '../';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobId, JobSummary } from '../../common/types';
import { JobId } from '../../common/types';
import { DownloadButton } from './job_download_button';
import { ReportLink } from './report_link';
@ -21,7 +22,7 @@ export const getWarningFormulasToast = (
<FormattedMessage
id="xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportTitle"
defaultMessage="Report may contain formulas {reportObjectType} '{reportObjectTitle}'"
values={{ reportObjectType: job.type, reportObjectTitle: job.title }}
values={{ reportObjectType: job.jobtype, reportObjectTitle: job.title }}
/>
),
text: toMountPoint(

View file

@ -7,8 +7,9 @@
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment } from 'react';
import { ToastInput } from 'src/core/public';
import { JobSummary } from '../';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobId, JobSummary } from '../../common/types';
import { JobId } from '../../common/types';
import { DownloadButton } from './job_download_button';
import { ReportLink } from './report_link';
@ -21,7 +22,7 @@ export const getWarningMaxSizeToast = (
<FormattedMessage
id="xpack.reporting.publicNotifier.maxSizeReached.partialReportTitle"
defaultMessage="Created partial report for {reportObjectType} '{reportObjectTitle}'"
values={{ reportObjectType: job.type, reportObjectTitle: job.title }}
values={{ reportObjectType: job.jobtype, reportObjectTitle: job.title }}
/>
),
text: toMountPoint(

View file

@ -41,17 +41,17 @@ export interface Job {
type: string;
object_type: string;
object_title: string;
created_by?: string;
created_by?: string | false;
created_at: string;
started_at?: string;
completed_at?: string;
status: string;
statusLabel: string;
max_size_reached: boolean;
max_size_reached?: boolean;
attempts: number;
max_attempts: number;
csv_contains_formulas: boolean;
warnings: string[];
warnings?: string[];
}
export interface Props {

View file

@ -7,10 +7,11 @@
import { EuiButton, EuiCopy, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React, { Component, ReactElement } from 'react';
import url from 'url';
import { ToastsSetup } from 'src/core/public';
import { ReportingAPIClient } from '../lib/reporting_api_client';
import url from 'url';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { BaseParams } from '../../common/types';
import { ReportingAPIClient } from '../lib/reporting_api_client';
interface Props {
apiClient: ReportingAPIClient;
@ -19,7 +20,7 @@ interface Props {
layoutId: string | undefined;
objectId?: string;
objectType: string;
getJobParams: () => any;
getJobParams: () => BaseParams;
options?: ReactElement<any>;
isDirty: boolean;
onClose: () => void;

View file

@ -4,12 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiSpacer, EuiSwitch } from '@elastic/eui';
import { EuiSpacer, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component, Fragment } from 'react';
import { ToastsSetup } from 'src/core/public';
import { ReportingPanelContent } from './reporting_panel_content';
import { BaseParams } from '../../common/types';
import { ReportingAPIClient } from '../lib/reporting_api_client';
import { ReportingPanelContent } from './reporting_panel_content';
interface Props {
apiClient: ReportingAPIClient;
@ -17,7 +18,7 @@ interface Props {
reportType: string;
objectId?: string;
objectType: string;
getJobParams: () => any;
getJobParams: () => BaseParams;
isDirty: boolean;
onClose: () => void;
}
@ -83,7 +84,7 @@ export class ScreenCapturePanelContent extends Component<Props, State> {
);
};
private handlePrintLayoutChange = (evt: any) => {
private handlePrintLayoutChange = (evt: EuiSwitchEvent) => {
this.setState({ usePrintLayout: evt.target.checked });
};

View file

@ -7,6 +7,7 @@
import { PluginInitializerContext } from 'src/core/public';
import { ReportingPublicPlugin } from './plugin';
import * as jobCompletionNotifications from './lib/job_completion_notifications';
import { JobId, JobStatus } from '../common/types';
export function plugin(initializerContext: PluginInitializerContext) {
return new ReportingPublicPlugin(initializerContext);
@ -14,3 +15,23 @@ export function plugin(initializerContext: PluginInitializerContext) {
export { ReportingPublicPlugin as Plugin };
export { jobCompletionNotifications };
export interface JobSummary {
id: JobId;
status: JobStatus;
title: string;
jobtype: string;
maxSizeReached?: boolean;
csvContainsFormulas?: boolean;
}
export interface JobSummarySet {
completed: JobSummary[];
failed: JobSummary[];
}
type DownloadLink = string;
export type DownloadReportFn = (jobId: JobId) => DownloadLink;
type ManagementLink = string;
export type ManagementLinkFn = () => ManagementLink;

View file

@ -6,20 +6,20 @@ Object {
Object {
"csvContainsFormulas": false,
"id": "job-source-mock1",
"jobtype": undefined,
"maxSizeReached": false,
"status": "completed",
"title": "specimen",
"type": "spectacular",
},
],
"failed": Array [
Object {
"csvContainsFormulas": false,
"id": "job-source-mock2",
"jobtype": undefined,
"maxSizeReached": false,
"status": "failed",
"title": "specimen",
"type": "spectacular",
},
],
}
@ -49,9 +49,9 @@ Array [
Object {
"csvContainsFormulas": true,
"id": "yas3",
"jobtype": "yas",
"status": "completed",
"title": "Yas",
"type": "yas",
}
}
/>
@ -149,10 +149,10 @@ Array [
job={
Object {
"id": "yas2",
"jobtype": "yas",
"maxSizeReached": true,
"status": "completed",
"title": "Yas",
"type": "yas",
}
}
/>
@ -191,9 +191,9 @@ Array [
job={
Object {
"id": "yas1",
"jobtype": "yas",
"status": "completed",
"title": "Yas",
"type": "yas",
}
}
/>

View file

@ -7,10 +7,11 @@
import { stringify } from 'query-string';
import rison from 'rison-node';
import { HttpSetup } from 'src/core/public';
import { JobId, SourceJob } from '../../common/types';
import { DownloadReportFn, ManagementLinkFn } from '../';
import { JobId, ReportApiJSON, ReportDocument, ReportSource } from '../../common/types';
import {
API_BASE_URL,
API_BASE_GENERATE,
API_BASE_URL,
API_LIST_URL,
REPORTING_MANAGEMENT_HOME,
} from '../../constants';
@ -18,7 +19,7 @@ import { add } from './job_completion_notifications';
export interface JobQueueEntry {
_id: string;
_source: any;
_source: ReportSource;
}
export interface JobContent {
@ -26,40 +27,6 @@ export interface JobContent {
content_type: boolean;
}
export interface JobInfo {
kibana_name: string;
kibana_id: string;
browser_type: string;
created_at: string;
priority: number;
jobtype: string;
created_by: string;
timeout: number;
output: {
content_type: string;
size: number;
warnings: string[];
};
process_expiration: string;
completed_at: string;
payload: {
layout: { id: string; dimensions: { width: number; height: number } };
objects: Array<{ relativeUrl: string }>;
type: string;
title: string;
forceNow: string;
browserTimezone: string;
};
meta: {
layout: string;
objectType: string;
};
max_attempts: number;
started_at: string;
attempts: number;
status: string;
}
interface JobParams {
[paramName: string]: any;
}
@ -121,13 +88,13 @@ export class ReportingAPIClient {
});
}
public getInfo(jobId: string): Promise<JobInfo> {
public getInfo(jobId: string): Promise<ReportApiJSON> {
return this.http.get(`${API_LIST_URL}/info/${jobId}`, {
asSystemRequest: true,
});
}
public findForJobIds = (jobIds: JobId[]): Promise<SourceJob[]> => {
public findForJobIds = (jobIds: JobId[]): Promise<ReportDocument[]> => {
return this.http.fetch(`${API_LIST_URL}/list`, {
query: { page: 0, ids: jobIds.join(',') },
method: 'GET',
@ -159,9 +126,10 @@ export class ReportingAPIClient {
return resp;
};
public getManagementLink = () => this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME);
public getManagementLink: ManagementLinkFn = () =>
this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME);
public getDownloadLink = (jobId: JobId) =>
public getDownloadLink: DownloadReportFn = (jobId: JobId) =>
this.http.basePath.prepend(`${API_LIST_URL}/download/${jobId}`);
/*

View file

@ -6,7 +6,8 @@
import sinon, { stub } from 'sinon';
import { NotificationsStart } from 'src/core/public';
import { JobSummary, SourceJob } from '../../common/types';
import { JobSummary } from '../';
import { ReportDocument } from '../../common/types';
import { ReportingAPIClient } from './reporting_api_client';
import { ReportingNotifierStreamHandler } from './stream_handler';
@ -23,7 +24,7 @@ const mockJobsFound = [
_source: {
status: 'completed',
output: { max_size_reached: false, csv_contains_formulas: false },
payload: { type: 'spectacular', title: 'specimen' },
payload: { title: 'specimen' },
},
},
{
@ -31,7 +32,7 @@ const mockJobsFound = [
_source: {
status: 'failed',
output: { max_size_reached: false, csv_contains_formulas: false },
payload: { type: 'spectacular', title: 'specimen' },
payload: { title: 'specimen' },
},
},
{
@ -39,14 +40,14 @@ const mockJobsFound = [
_source: {
status: 'pending',
output: { max_size_reached: false, csv_contains_formulas: false },
payload: { type: 'spectacular', title: 'specimen' },
payload: { title: 'specimen' },
},
},
];
const jobQueueClientMock: ReportingAPIClient = {
findForJobIds: async (jobIds: string[]) => {
return mockJobsFound as SourceJob[];
return mockJobsFound as ReportDocument[];
},
getContent: (): Promise<any> => {
return Promise.resolve({ content: 'this is the completed report data' });
@ -109,7 +110,7 @@ describe('stream handler', () => {
{
id: 'yas1',
title: 'Yas',
type: 'yas',
jobtype: 'yas',
status: 'completed',
} as JobSummary,
],
@ -130,7 +131,7 @@ describe('stream handler', () => {
{
id: 'yas2',
title: 'Yas',
type: 'yas',
jobtype: 'yas',
status: 'completed',
maxSizeReached: true,
} as JobSummary,
@ -152,7 +153,7 @@ describe('stream handler', () => {
{
id: 'yas3',
title: 'Yas',
type: 'yas',
jobtype: 'yas',
status: 'completed',
csvContainsFormulas: true,
} as JobSummary,
@ -175,7 +176,7 @@ describe('stream handler', () => {
{
id: 'yas7',
title: 'Yas 7',
type: 'yas',
jobtype: 'yas',
status: 'failed',
} as JobSummary,
],
@ -195,20 +196,20 @@ describe('stream handler', () => {
{
id: 'yas8',
title: 'Yas 8',
type: 'yas',
jobtype: 'yas',
status: 'completed',
} as JobSummary,
{
id: 'yas9',
title: 'Yas 9',
type: 'yas',
jobtype: 'yas',
status: 'completed',
csvContainsFormulas: true,
} as JobSummary,
{
id: 'yas10',
title: 'Yas 10',
type: 'yas',
jobtype: 'yas',
status: 'completed',
maxSizeReached: true,
} as JobSummary,
@ -217,7 +218,7 @@ describe('stream handler', () => {
{
id: 'yas13',
title: 'Yas 13',
type: 'yas',
jobtype: 'yas',
status: 'failed',
} as JobSummary,
],

View file

@ -8,7 +8,8 @@ import { i18n } from '@kbn/i18n';
import * as Rx from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { NotificationsSetup } from 'src/core/public';
import { JobId, JobStatusBuckets, JobSummary, SourceJob } from '../../common/types';
import { JobSummarySet, JobSummary } from '../';
import { JobId, ReportDocument } from '../../common/types';
import {
JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY,
JOB_STATUS_COMPLETED,
@ -28,14 +29,14 @@ function updateStored(jobIds: JobId[]): void {
sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobIds));
}
function summarizeJob(src: SourceJob): JobSummary {
function getReportStatus(src: ReportDocument): JobSummary {
return {
id: src._id,
status: src._source.status,
title: src._source.payload.title,
type: src._source.payload.type,
maxSizeReached: src._source.output.max_size_reached,
csvContainsFormulas: src._source.output.csv_contains_formulas,
jobtype: src._source.jobtype,
maxSizeReached: src._source.output?.max_size_reached,
csvContainsFormulas: src._source.output?.csv_contains_formulas,
};
}
@ -48,7 +49,7 @@ export class ReportingNotifierStreamHandler {
public showNotifications({
completed: completedJobs,
failed: failedJobs,
}: JobStatusBuckets): Rx.Observable<JobStatusBuckets> {
}: JobSummarySet): Rx.Observable<JobSummarySet> {
const showNotificationsAsync = async () => {
// notifications with download link
for (const job of completedJobs) {
@ -92,9 +93,9 @@ export class ReportingNotifierStreamHandler {
* An observable that finds jobs that are known to be "processing" (stored in
* session storage) but have non-processing job status on the server
*/
public findChangedStatusJobs(storedJobs: JobId[]): Rx.Observable<JobStatusBuckets> {
public findChangedStatusJobs(storedJobs: JobId[]): Rx.Observable<JobSummarySet> {
return Rx.from(this.apiClient.findForJobIds(storedJobs)).pipe(
map((jobs: SourceJob[]) => {
map((jobs: ReportDocument[]) => {
const completedJobs: JobSummary[] = [];
const failedJobs: JobSummary[] = [];
const pending: JobId[] = [];
@ -107,9 +108,9 @@ export class ReportingNotifierStreamHandler {
} = job;
if (storedJobs.includes(jobId)) {
if (jobStatus === JOB_STATUS_COMPLETED || jobStatus === JOB_STATUS_WARNINGS) {
completedJobs.push(summarizeJob(job));
completedJobs.push(getReportStatus(job));
} else if (jobStatus === JOB_STATUS_FAILED) {
failedJobs.push(summarizeJob(job));
failedJobs.push(getReportStatus(job));
} else {
pending.push(jobId);
}

View file

@ -27,8 +27,9 @@ import { ManagementSetup } from '../../../../src/plugins/management/public';
import { SharePluginSetup } from '../../../../src/plugins/share/public';
import { LicensingPluginSetup } from '../../licensing/public';
import { durationToNumber } from '../common/schema_utils';
import { JobId, JobStatusBuckets, ReportingConfigType } from '../common/types';
import { JobId, ReportingConfigType } from '../common/types';
import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants';
import { JobSummarySet } from './';
import { getGeneralErrorToast } from './components';
import { ReportListing } from './components/report_listing';
import { ReportingAPIClient } from './lib/reporting_api_client';
@ -46,10 +47,7 @@ function getStored(): JobId[] {
return sessionValue ? JSON.parse(sessionValue) : [];
}
function handleError(
notifications: NotificationsSetup,
err: Error
): Rx.Observable<JobStatusBuckets> {
function handleError(notifications: NotificationsSetup, err: Error): Rx.Observable<JobSummarySet> {
notifications.toasts.addDanger(
getGeneralErrorToast(
i18n.translate('xpack.reporting.publicNotifier.pollingErrorMessage', {

View file

@ -10,7 +10,7 @@ import React from 'react';
import { IUiSettingsClient, ToastsSetup } from 'src/core/public';
import { ShareContext } from '../../../../../src/plugins/share/public';
import { LicensingPluginSetup } from '../../../licensing/public';
import { JobParamsDiscoverCsv, SearchRequest } from '../../server/export_types/csv/types';
import { JobParamsCSV, SearchRequest } from '../../server/export_types/csv/types';
import { ReportingPanelContent } from '../components/reporting_panel_content';
import { checkLicense } from '../lib/license_check';
import { ReportingAPIClient } from '../lib/reporting_api_client';
@ -59,7 +59,7 @@ export const csvReportingProvider = ({
return [];
}
const jobParams: JobParamsDiscoverCsv = {
const jobParams: JobParamsCSV = {
browserTimezone,
objectType,
title: sharingData.title as string,

View file

@ -10,7 +10,7 @@ import React from 'react';
import { IUiSettingsClient, ToastsSetup } from 'src/core/public';
import { ShareContext } from '../../../../../src/plugins/share/public';
import { LicensingPluginSetup } from '../../../licensing/public';
import { LayoutInstance } from '../../common/types';
import { LayoutParams } from '../../common/types';
import { JobParamsPNG } from '../../server/export_types/png/types';
import { JobParamsPDF } from '../../server/export_types/printable_pdf/types';
import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content';
@ -80,7 +80,7 @@ export const reportingPDFPNGProvider = ({
objectType,
browserTimezone,
relativeUrls: [relativeUrl], // multi URL for PDF
layout: sharingData.layout as LayoutInstance,
layout: sharingData.layout as LayoutParams,
title: sharingData.title as string,
};
};
@ -96,7 +96,7 @@ export const reportingPDFPNGProvider = ({
objectType,
browserTimezone,
relativeUrl, // single URL for PNG
layout: sharingData.layout as LayoutInstance,
layout: sharingData.layout as LayoutParams,
title: sharingData.title as string,
};
};

View file

@ -10,10 +10,10 @@ import open from 'opn';
import { ElementHandle, EvaluateFn, Page, Response, SerializableOrJSHandle } from 'puppeteer';
import { parse as parseUrl } from 'url';
import { getDisallowedOutgoingUrlError } from '../';
import { ConditionalHeaders, ConditionalHeadersConditions } from '../../../export_types/common';
import { LevelLogger } from '../../../lib';
import { ViewZoomWidthHeight } from '../../../lib/layouts/layout';
import { ElementPosition } from '../../../lib/screenshots';
import { ConditionalHeaders } from '../../../types';
import { allowRequest, NetworkPolicy } from '../../network_policy';
export interface ChromiumDriverOptions {
@ -34,8 +34,6 @@ interface EvaluateMetaOpts {
context: string;
}
type ConditionalHeadersConditions = ConditionalHeaders['conditions'];
interface InterceptedRequest {
requestId: string;
request: {

View file

@ -64,7 +64,7 @@ export class HeadlessChromiumDriverFactory {
* Return an observable to objects which will drive screenshot capture for a page
*/
createPage(
{ viewport, browserTimezone }: { viewport: ViewportConfig; browserTimezone: string },
{ viewport, browserTimezone }: { viewport: ViewportConfig; browserTimezone?: string },
pLogger: LevelLogger
): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable<never> }> {
return Rx.Observable.create(async (observer: InnerSubscriber<any, any>) => {

View file

@ -4,9 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { cryptoFactory, LevelLogger } from '../../lib';
import { cryptoFactory } from '../../lib';
import { createMockLevelLogger } from '../../test_helpers';
import { decryptJobHeaders } from './';
const logger = createMockLevelLogger();
const encryptHeaders = async (encryptionKey: string, headers: Record<string, string>) => {
const crypto = cryptoFactory(encryptionKey);
return await crypto.encrypt(headers);
@ -15,15 +18,11 @@ const encryptHeaders = async (encryptionKey: string, headers: Record<string, str
describe('headers', () => {
test(`fails if it can't decrypt headers`, async () => {
const getDecryptedHeaders = () =>
decryptJobHeaders({
encryptionKey: 'abcsecretsauce',
job: {
headers: 'Q53+9A+zf+Xe+ceR/uB/aR/Sw/8e+M+qR+WiG+8z+EY+mo+HiU/zQL+Xn',
},
logger: ({
error: jest.fn(),
} as unknown) as LevelLogger,
});
decryptJobHeaders(
'abcsecretsauce',
'Q53+9A+zf+Xe+ceR/uB/aR/Sw/8e+M+qR+WiG+8z+EY+mo+HiU/zQL+Xn',
logger
);
await expect(getDecryptedHeaders()).rejects.toMatchInlineSnapshot(
`[Error: Failed to decrypt report job data. Please ensure that xpack.reporting.encryptionKey is set and re-generate this report. Error: Invalid IV length]`
);
@ -36,15 +35,7 @@ describe('headers', () => {
};
const encryptedHeaders = await encryptHeaders('abcsecretsauce', headers);
const decryptedHeaders = await decryptJobHeaders({
encryptionKey: 'abcsecretsauce',
job: {
title: 'cool-job-bro',
type: 'csv',
headers: encryptedHeaders,
},
logger: {} as LevelLogger,
});
const decryptedHeaders = await decryptJobHeaders('abcsecretsauce', encryptedHeaders, logger);
expect(decryptedHeaders).toEqual(headers);
});
});

View file

@ -7,24 +7,13 @@
import { i18n } from '@kbn/i18n';
import { cryptoFactory, LevelLogger } from '../../lib';
interface HasEncryptedHeaders {
headers?: string;
}
export const decryptJobHeaders = async <
JobParamsType,
TaskPayloadType extends HasEncryptedHeaders
>({
encryptionKey,
job,
logger,
}: {
encryptionKey?: string;
job: TaskPayloadType;
logger: LevelLogger;
}): Promise<Record<string, string>> => {
export const decryptJobHeaders = async (
encryptionKey: string | undefined,
headers: string,
logger: LevelLogger
): Promise<Record<string, string>> => {
try {
if (typeof job.headers !== 'string') {
if (typeof headers !== 'string') {
throw new Error(
i18n.translate('xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage', {
defaultMessage: 'Job headers are missing',
@ -32,7 +21,7 @@ export const decryptJobHeaders = async <
);
}
const crypto = cryptoFactory(encryptionKey);
const decryptedHeaders = (await crypto.decrypt(job.headers)) as Record<string, string>;
const decryptedHeaders = (await crypto.decrypt(headers)) as Record<string, string>;
return decryptedHeaders;
} catch (err) {
logger.error(err);

View file

@ -6,7 +6,6 @@
import { ReportingConfig } from '../../';
import { createMockConfig, createMockConfigSchema } from '../../test_helpers';
import { BasePayload } from '../../types';
import { getConditionalHeaders } from './';
let mockConfig: ReportingConfig;
@ -24,11 +23,7 @@ describe('conditions', () => {
baz: 'quix',
};
const conditionalHeaders = getConditionalHeaders({
job: {} as BasePayload<any>,
filteredHeaders: permittedHeaders,
config: mockConfig,
});
const conditionalHeaders = getConditionalHeaders(mockConfig, permittedHeaders);
expect(conditionalHeaders.conditions.hostname).toEqual(
mockConfig.get('kibanaServer', 'hostname')
@ -49,19 +44,7 @@ describe('config formatting', () => {
const mockSchema = createMockConfigSchema(reportingConfig);
mockConfig = createMockConfig(mockSchema);
const conditionalHeaders = getConditionalHeaders({
job: {
title: 'cool-job-bro',
type: 'csv',
jobParams: {
savedObjectId: 'abc-123',
isImmediate: false,
savedObjectType: 'search',
},
},
filteredHeaders: {},
config: mockConfig,
});
const conditionalHeaders = getConditionalHeaders(mockConfig, {});
expect(conditionalHeaders.conditions.hostname).toEqual('great-hostname');
});
});

View file

@ -5,17 +5,12 @@
*/
import { ReportingConfig } from '../../';
import { ConditionalHeaders } from '../../types';
import { ConditionalHeaders } from './';
export const getConditionalHeaders = <TaskPayloadType>({
config,
job,
filteredHeaders,
}: {
config: ReportingConfig;
job: TaskPayloadType;
filteredHeaders: Record<string, string>;
}) => {
export const getConditionalHeaders = (
config: ReportingConfig,
filteredHeaders: Record<string, string>
) => {
const { kbnConfig } = config;
const [hostname, port, basePath, protocol] = [
config.get('kibanaServer', 'hostname'),

View file

@ -10,11 +10,6 @@ import { TaskPayloadPNG } from '../png/types';
import { TaskPayloadPDF } from '../printable_pdf/types';
import { getFullUrls } from './get_full_urls';
interface FullUrlsOpts {
job: TaskPayloadPNG & TaskPayloadPDF;
config: ReportingConfig;
}
let mockConfig: ReportingConfig;
beforeEach(() => {
@ -30,7 +25,7 @@ beforeEach(() => {
const getMockJob = (base: object) => base as TaskPayloadPNG & TaskPayloadPDF;
test(`fails if no URL is passed`, async () => {
const fn = () => getFullUrls({ job: getMockJob({}), config: mockConfig } as FullUrlsOpts);
const fn = () => getFullUrls(mockConfig, getMockJob({}));
expect(fn).toThrowErrorMatchingInlineSnapshot(
`"No valid URL fields found in Job Params! Expected \`job.relativeUrl: string\` or \`job.relativeUrls: string[]\`"`
);
@ -39,11 +34,7 @@ test(`fails if no URL is passed`, async () => {
test(`fails if URLs are file-protocols for PNGs`, async () => {
const forceNow = '2000-01-01T00:00:00.000Z';
const relativeUrl = 'file://etc/passwd/#/something';
const fn = () =>
getFullUrls({
job: getMockJob({ relativeUrl, forceNow }),
config: mockConfig,
} as FullUrlsOpts);
const fn = () => getFullUrls(mockConfig, getMockJob({ relativeUrl, forceNow }));
expect(fn).toThrowErrorMatchingInlineSnapshot(
`"Found invalid URL(s), all URLs must be relative: file://etc/passwd/#/something"`
);
@ -53,11 +44,7 @@ test(`fails if URLs are absolute for PNGs`, async () => {
const forceNow = '2000-01-01T00:00:00.000Z';
const relativeUrl =
'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something';
const fn = () =>
getFullUrls({
job: getMockJob({ relativeUrl, forceNow }),
config: mockConfig,
} as FullUrlsOpts);
const fn = () => getFullUrls(mockConfig, getMockJob({ relativeUrl, forceNow }));
expect(fn).toThrowErrorMatchingInlineSnapshot(
`"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something"`
);
@ -67,13 +54,13 @@ test(`fails if URLs are file-protocols for PDF`, async () => {
const forceNow = '2000-01-01T00:00:00.000Z';
const relativeUrl = 'file://etc/passwd/#/something';
const fn = () =>
getFullUrls({
job: getMockJob({
getFullUrls(
mockConfig,
getMockJob({
relativeUrls: [relativeUrl],
forceNow,
}),
config: mockConfig,
} as FullUrlsOpts);
})
);
expect(fn).toThrowErrorMatchingInlineSnapshot(
`"Found invalid URL(s), all URLs must be relative: file://etc/passwd/#/something"`
);
@ -84,13 +71,13 @@ test(`fails if URLs are absolute for PDF`, async () => {
const relativeUrl =
'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something';
const fn = () =>
getFullUrls({
job: getMockJob({
getFullUrls(
mockConfig,
getMockJob({
relativeUrls: [relativeUrl],
forceNow,
}),
config: mockConfig,
} as FullUrlsOpts);
})
);
expect(fn).toThrowErrorMatchingInlineSnapshot(
`"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something"`
);
@ -104,22 +91,14 @@ test(`fails if any URLs are absolute or file's for PDF`, async () => {
'file://etc/passwd/#/something',
];
const fn = () =>
getFullUrls({
job: getMockJob({ relativeUrls, forceNow }),
config: mockConfig,
} as FullUrlsOpts);
const fn = () => getFullUrls(mockConfig, getMockJob({ relativeUrls, forceNow }));
expect(fn).toThrowErrorMatchingInlineSnapshot(
`"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something file://etc/passwd/#/something"`
);
});
test(`fails if URL does not route to a visualization`, async () => {
const fn = () =>
getFullUrls({
job: getMockJob({ relativeUrl: '/app/phoney' }),
config: mockConfig,
} as FullUrlsOpts);
const fn = () => getFullUrls(mockConfig, getMockJob({ relativeUrl: '/app/phoney' }));
expect(fn).toThrowErrorMatchingInlineSnapshot(
`"No valid hash in the URL! A hash is expected for the application to route to the intended visualization."`
);
@ -127,10 +106,10 @@ test(`fails if URL does not route to a visualization`, async () => {
test(`adds forceNow to hash's query, if it exists`, async () => {
const forceNow = '2000-01-01T00:00:00.000Z';
const urls = await getFullUrls({
job: getMockJob({ relativeUrl: '/app/kibana#/something', forceNow }),
config: mockConfig,
} as FullUrlsOpts);
const urls = await getFullUrls(
mockConfig,
getMockJob({ relativeUrl: '/app/kibana#/something', forceNow })
);
expect(urls[0]).toEqual(
'http://localhost:5601/sbp/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z'
@ -140,10 +119,10 @@ test(`adds forceNow to hash's query, if it exists`, async () => {
test(`appends forceNow to hash's query, if it exists`, async () => {
const forceNow = '2000-01-01T00:00:00.000Z';
const urls = await getFullUrls({
job: getMockJob({ relativeUrl: '/app/kibana#/something?_g=something', forceNow }),
config: mockConfig,
} as FullUrlsOpts);
const urls = await getFullUrls(
mockConfig,
getMockJob({ relativeUrl: '/app/kibana#/something?_g=something', forceNow })
);
expect(urls[0]).toEqual(
'http://localhost:5601/sbp/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z'
@ -151,18 +130,16 @@ 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 getFullUrls({
job: getMockJob({ relativeUrl: '/app/kibana#/something' }),
config: mockConfig,
} as FullUrlsOpts);
const urls = await getFullUrls(mockConfig, getMockJob({ relativeUrl: '/app/kibana#/something' }));
expect(urls[0]).toEqual('http://localhost:5601/sbp/app/kibana#/something');
});
test(`adds forceNow to each of multiple urls`, async () => {
const forceNow = '2000-01-01T00:00:00.000Z';
const urls = await getFullUrls({
job: getMockJob({
const urls = await getFullUrls(
mockConfig,
getMockJob({
relativeUrls: [
'/app/kibana#/something_aaa',
'/app/kibana#/something_bbb',
@ -170,9 +147,8 @@ test(`adds forceNow to each of multiple urls`, async () => {
'/app/kibana#/something_ddd',
],
forceNow,
}),
config: mockConfig,
} as FullUrlsOpts);
})
);
expect(urls).toEqual([
'http://localhost:5601/sbp/app/kibana#/something_aaa?forceNow=2000-01-01T00%3A00%3A00.000Z',

View file

@ -23,13 +23,7 @@ function isPdfJob(job: TaskPayloadPNG | TaskPayloadPDF): job is TaskPayloadPDF {
return (job as TaskPayloadPDF).relativeUrls !== undefined;
}
export function getFullUrls<TaskPayloadType>({
config,
job,
}: {
config: ReportingConfig;
job: TaskPayloadPDF | TaskPayloadPNG;
}) {
export function getFullUrls(config: ReportingConfig, job: TaskPayloadPDF | TaskPayloadPNG) {
const [basePath, protocol, hostname, port] = [
config.kbnConfig.get('server', 'basePath'),
config.get('kibanaServer', 'protocol'),

View file

@ -9,3 +9,21 @@ export { getConditionalHeaders } from './get_conditional_headers';
export { getFullUrls } from './get_full_urls';
export { omitBlockedHeaders } from './omit_blocked_headers';
export { validateUrls } from './validate_urls';
export interface TimeRangeParams {
timezone: string;
min?: Date | string | number | null;
max?: Date | string | number | null;
}
export interface ConditionalHeadersConditions {
protocol: string;
hostname: string;
port: number;
basePath: string;
}
export interface ConditionalHeaders {
headers: Record<string, string>;
conditions: ConditionalHeadersConditions;
}

View file

@ -24,20 +24,9 @@ test(`omits blocked headers`, async () => {
trailer: 's are for trucks',
};
const filteredHeaders = await omitBlockedHeaders({
job: {
title: 'cool-job-bro',
type: 'csv',
jobParams: {
savedObjectId: 'abc-123',
isImmediate: false,
savedObjectType: 'search',
},
},
decryptedHeaders: {
...permittedHeaders,
...blockedHeaders,
},
const filteredHeaders = omitBlockedHeaders({
...permittedHeaders,
...blockedHeaders,
});
expect(filteredHeaders).toEqual(permittedHeaders);

View file

@ -9,13 +9,7 @@ import {
KBN_SCREENSHOT_HEADER_BLOCK_LIST_STARTS_WITH_PATTERN,
} from '../../../common/constants';
export const omitBlockedHeaders = <TaskPayloadType>({
job,
decryptedHeaders,
}: {
job: TaskPayloadType;
decryptedHeaders: Record<string, string>;
}) => {
export const omitBlockedHeaders = (decryptedHeaders: Record<string, string>) => {
const filteredHeaders: Record<string, string> = omitBy(
decryptedHeaders,
(_value, header: string) =>

View file

@ -6,10 +6,11 @@
import { cryptoFactory } from '../../lib';
import { CreateJobFn, CreateJobFnFactory } from '../../types';
import { JobParamsDiscoverCsv } from './types';
import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types';
export const createJobFnFactory: CreateJobFnFactory<CreateJobFn<
JobParamsDiscoverCsv
JobParamsCSV,
TaskPayloadCSV
>> = function createJobFactoryFn(reporting) {
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
@ -18,10 +19,10 @@ export const createJobFnFactory: CreateJobFnFactory<CreateJobFn<
const serializedEncryptedHeaders = await crypto.encrypt(request.headers);
const savedObjectsClient = context.core.savedObjects.client;
const indexPatternSavedObject = await savedObjectsClient.get(
const indexPatternSavedObject = ((await savedObjectsClient.get(
'index-pattern',
jobParams.indexPatternId
);
)) as unknown) as IndexPatternSavedObject; // FIXME
return {
headers: serializedEncryptedHeaders,

View file

@ -22,7 +22,7 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<
const generateCsv = createGenerateCsv(jobLogger);
const encryptionKey = config.get('encryptionKey');
const headers = await decryptJobHeaders({ encryptionKey, job, logger });
const headers = await decryptJobHeaders(encryptionKey, job.headers, logger);
const fakeRequest = reporting.getFakeRequest({ headers }, job.spaceId);
const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest);

View file

@ -37,7 +37,7 @@ interface SearchRequest {
}
export interface GenerateCsvParams {
browserTimezone: string;
browserTimezone?: string;
searchRequest: SearchRequest;
indexPatternSavedObject: IndexPatternSavedObject;
fields: string[];

View file

@ -17,12 +17,10 @@ import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types';
import { createJobFnFactory } from './create_job';
import { runTaskFnFactory } from './execute_job';
import { metadata } from './metadata';
import { JobParamsDiscoverCsv, TaskPayloadCSV } from './types';
import { JobParamsCSV, TaskPayloadCSV } from './types';
export const getExportType = (): ExportTypeDefinition<
JobParamsDiscoverCsv,
CreateJobFn<JobParamsDiscoverCsv>,
TaskPayloadCSV,
CreateJobFn<JobParamsCSV>,
RunTaskFn<TaskPayloadCSV>
> => ({
...metadata,

View file

@ -8,16 +8,6 @@ import { BaseParams, BasePayload } from '../../types';
export type RawValue = string | object | null | undefined;
interface DocValueField {
field: string;
format: string;
}
interface SortOptions {
order: string;
unmapped_type: string;
}
export interface IndexPatternSavedObject {
title: string;
timeFieldName: string;
@ -28,25 +18,23 @@ export interface IndexPatternSavedObject {
};
}
export interface JobParamsDiscoverCsv extends BaseParams {
browserTimezone: string;
indexPatternId: string;
title: string;
interface BaseParamsCSV {
searchRequest: SearchRequest;
fields: string[];
metaFields: string[];
conflictedTypesFields: string[];
}
export interface TaskPayloadCSV extends BasePayload<JobParamsDiscoverCsv> {
browserTimezone: string;
basePath: string;
searchRequest: any;
fields: any;
indexPatternSavedObject: any;
metaFields: any;
conflictedTypesFields: any;
}
export type JobParamsCSV = BaseParamsCSV &
BaseParams & {
indexPatternId: string;
};
// CSV create job method converts indexPatternID to indexPatternSavedObject
export type TaskPayloadCSV = BaseParamsCSV &
BasePayload & {
indexPatternSavedObject: IndexPatternSavedObject;
};
export interface SearchRequest {
index: string;

View file

@ -6,57 +6,40 @@
import { notFound, notImplemented } from 'boom';
import { get } from 'lodash';
import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
import { RequestHandlerContext } from 'src/core/server';
import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants';
import { cryptoFactory } from '../../lib';
import { CreateJobFnFactory, TimeRangeParams } from '../../types';
import { CsvFromSavedObjectRequest } from '../../routes/generate_from_savedobject_immediate';
import { CreateJobFnFactory } from '../../types';
import {
JobParamsPanelCsv,
JobPayloadPanelCsv,
SavedObject,
SavedObjectReference,
SavedObjectServiceError,
SavedSearchObjectAttributesJSON,
SearchPanel,
VisObjectAttributesJSON,
} from './types';
export type ImmediateCreateJobFn = (
jobParams: JobParamsPanelCsv,
headers: KibanaRequest['headers'],
context: RequestHandlerContext,
req: KibanaRequest
) => Promise<{
type: string;
title: string;
jobParams: JobParamsPanelCsv;
}>;
interface VisData {
title: string;
visType: string;
panel: SearchPanel;
}
req: CsvFromSavedObjectRequest
) => Promise<JobPayloadPanelCsv>;
export const createJobFnFactory: CreateJobFnFactory<ImmediateCreateJobFn> = function createJobFactoryFn(
reporting,
parentLogger
) {
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']);
return async function createJob(jobParams, headers, context, req) {
return async function createJob(jobParams, context, req) {
const { savedObjectType, savedObjectId } = jobParams;
const serializedEncryptedHeaders = await crypto.encrypt(headers);
const { panel, title, visType }: VisData = await Promise.resolve()
const panel = await Promise.resolve()
.then(() => context.core.savedObjects.client.get(savedObjectType, savedObjectId))
.then(async (savedObject: SavedObject) => {
const { attributes, references } = savedObject;
const {
kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON,
} = attributes as SavedSearchObjectAttributesJSON;
const { timerange } = req.body as { timerange: TimeRangeParams };
const { kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON } = attributes;
const { timerange } = req.body;
if (!kibanaSavedObjectMetaJSON) {
throw new Error('Could not parse saved object data!');
@ -85,7 +68,7 @@ export const createJobFnFactory: CreateJobFnFactory<ImmediateCreateJobFn> = func
throw new Error('Could not find index pattern for the saved search!');
}
const sPanel = {
return {
attributes: {
...attributes,
kibanaSavedObjectMeta: { searchSource },
@ -93,8 +76,6 @@ export const createJobFnFactory: CreateJobFnFactory<ImmediateCreateJobFn> = func
indexPatternSavedObjectId: indexPatternMeta.id,
timerange,
};
return { panel: sPanel, title: attributes.title, visType: 'search' };
})
.catch((err: Error) => {
const boomErr = (err as unknown) as { isBoom: boolean };
@ -109,11 +90,6 @@ export const createJobFnFactory: CreateJobFnFactory<ImmediateCreateJobFn> = func
throw new Error(`Unable to create a job from saved object data! Error: ${err}`);
});
return {
headers: serializedEncryptedHeaders,
jobParams: { ...jobParams, panel, visType },
type: visType,
title,
};
return { ...jobParams, panel };
};
};

View file

@ -7,16 +7,11 @@
import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
import { CancellationToken } from '../../../common';
import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants';
import { BasePayload, RunTaskFnFactory, TaskRunResult } from '../../types';
import { TaskRunResult } from '../../lib/tasks';
import { RunTaskFnFactory } from '../../types';
import { createGenerateCsv } from '../csv/generate_csv';
import { getGenerateCsvParams } from './lib/get_csv_job';
import { JobParamsPanelCsv, SearchPanel } from './types';
/*
* The run function receives the full request which provides the un-encrypted
* headers, so encrypted headers are not part of these kind of job params
*/
type ImmediateJobParams = Omit<BasePayload<JobParamsPanelCsv>, 'headers'>;
import { JobPayloadPanelCsv } from './types';
/*
* ImmediateExecuteFn receives the job doc payload because the payload was
@ -24,7 +19,7 @@ type ImmediateJobParams = Omit<BasePayload<JobParamsPanelCsv>, 'headers'>;
*/
export type ImmediateExecuteFn = (
jobId: null,
job: ImmediateJobParams,
job: JobPayloadPanelCsv,
context: RequestHandlerContext,
req: KibanaRequest
) => Promise<TaskRunResult>;
@ -36,20 +31,16 @@ export const runTaskFnFactory: RunTaskFnFactory<ImmediateExecuteFn> = function e
const config = reporting.getConfig();
const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']);
return async function runTask(jobId: string | null, jobPayload, context, req) {
// There will not be a jobID for "immediate" generation.
// jobID is only for "queued" jobs
// Use the jobID as a logging tag or "immediate"
const { jobParams } = jobPayload;
return async function runTask(jobId, jobPayload, context, req) {
const jobLogger = logger.clone(['immediate']);
const generateCsv = createGenerateCsv(jobLogger);
const { panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel };
const { panel, visType } = jobPayload;
jobLogger.debug(`Execute job generating [${visType}] csv`);
const savedObjectsClient = context.core.savedObjects.client;
const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient);
const job = await getGenerateCsvParams(jobParams, panel, savedObjectsClient, uiSettingsClient);
const job = await getGenerateCsvParams(jobPayload, panel, savedObjectsClient, uiSettingsClient);
const elasticsearch = reporting.getElasticsearchService();
const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(req);

View file

@ -17,7 +17,6 @@ import { ExportTypeDefinition } from '../../types';
import { createJobFnFactory, ImmediateCreateJobFn } from './create_job';
import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job';
import { metadata } from './metadata';
import { JobParamsPanelCsv } from './types';
/*
* These functions are exported to share with the API route handler that
@ -27,9 +26,7 @@ export { createJobFnFactory } from './create_job';
export { runTaskFnFactory } from './execute_job';
export const getExportType = (): ExportTypeDefinition<
JobParamsPanelCsv,
ImmediateCreateJobFn,
JobParamsPanelCsv,
ImmediateExecuteFn
> => ({
...metadata,

View file

@ -13,7 +13,7 @@ describe('Get CSV Job', () => {
let mockSavedObjectsClient: any;
let mockUiSettingsClient: any;
beforeEach(() => {
mockJobParams = { isImmediate: true, savedObjectType: 'search', savedObjectId: '234-ididid' };
mockJobParams = { savedObjectType: 'search', savedObjectId: '234-ididid' };
mockSearchPanel = {
indexPatternSavedObjectId: '123-indexId',
attributes: {

View file

@ -12,7 +12,7 @@ import {
IIndexPattern,
Query,
} from '../../../../../../../src/plugins/data/server';
import { TimeRangeParams } from '../../../types';
import { TimeRangeParams } from '../../common';
import { GenerateCsvParams } from '../../csv/generate_csv';
import {
DocValueFields,
@ -50,11 +50,11 @@ export const getGenerateCsvParams = async (
savedObjectsClient: SavedObjectsClientContract,
uiConfig: IUiSettingsClient
): Promise<GenerateCsvParams> => {
let timerange: TimeRangeParams;
let timerange: TimeRangeParams | null;
if (jobParams.post?.timerange) {
timerange = jobParams.post?.timerange;
} else {
timerange = panel.timerange;
timerange = panel.timerange || null;
}
const { indexPatternSavedObjectId } = panel;
const savedSearchObjectAttr = panel.attributes as SavedSearchObjectAttributes;
@ -137,7 +137,7 @@ export const getGenerateCsvParams = async (
};
return {
browserTimezone: timerange.timezone,
browserTimezone: timerange?.timezone,
indexPatternSavedObject,
searchRequest,
fields: includes,

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TimeRangeParams } from '../../../types';
import { TimeRangeParams } from '../../common';
import { QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../types';
import { getFilters } from './get_filters';

View file

@ -6,7 +6,7 @@
import { badRequest } from 'boom';
import moment from 'moment-timezone';
import { TimeRangeParams } from '../../../types';
import { TimeRangeParams } from '../../common';
import { Filter, QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../types';
export function getFilters(

View file

@ -4,20 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { JobParamPostPayload, TimeRangeParams } from '../../types';
import { TimeRangeParams } from '../common';
export interface FakeRequest {
headers: Record<string, unknown>;
headers: Record<string, string>;
}
export interface JobParamsPostPayloadPanelCsv extends JobParamPostPayload {
export interface JobParamsPanelCsvPost {
timerange?: TimeRangeParams;
state?: any;
}
export interface SearchPanel {
indexPatternSavedObjectId: string;
attributes: SavedSearchObjectAttributes;
timerange: TimeRangeParams;
timerange?: TimeRangeParams;
}
export interface JobPayloadPanelCsv extends JobParamsPanelCsv {
@ -27,8 +28,7 @@ export interface JobPayloadPanelCsv extends JobParamsPanelCsv {
export interface JobParamsPanelCsv {
savedObjectType: string;
savedObjectId: string;
isImmediate: boolean;
post?: JobParamsPostPayloadPanelCsv;
post?: JobParamsPanelCsvPost;
visType?: string;
}

View file

@ -7,10 +7,11 @@
import { cryptoFactory } from '../../../lib';
import { CreateJobFn, CreateJobFnFactory } from '../../../types';
import { validateUrls } from '../../common';
import { JobParamsPNG } from '../types';
import { JobParamsPNG, TaskPayloadPNG } from '../types';
export const createJobFnFactory: CreateJobFnFactory<CreateJobFn<
JobParamsPNG
JobParamsPNG,
TaskPayloadPNG
>> = function createJobFactoryFn(reporting) {
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));

View file

@ -8,7 +8,8 @@ import apm from 'elastic-apm-node';
import * as Rx from 'rxjs';
import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators';
import { PNG_JOB_TYPE } from '../../../../common/constants';
import { RunTaskFn, RunTaskFnFactory, TaskRunResult } from '../../..//types';
import { TaskRunResult } from '../../../lib/tasks';
import { RunTaskFn, RunTaskFnFactory } from '../../../types';
import {
decryptJobHeaders,
getConditionalHeaders,
@ -18,12 +19,9 @@ import {
import { generatePngObservableFactory } from '../lib/generate_png';
import { TaskPayloadPNG } from '../types';
type QueuedPngExecutorFactory = RunTaskFnFactory<RunTaskFn<TaskPayloadPNG>>;
export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFactoryFn(
reporting,
parentLogger
) {
export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<
TaskPayloadPNG
>> = function executeJobFactoryFn(reporting, parentLogger) {
const config = reporting.getConfig();
const encryptionKey = config.get('encryptionKey');
const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']);
@ -36,11 +34,11 @@ export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFac
const generatePngObservable = await generatePngObservableFactory(reporting);
const jobLogger = logger.clone([jobId]);
const process$: Rx.Observable<TaskRunResult> = Rx.of(1).pipe(
mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })),
map((decryptedHeaders) => omitBlockedHeaders({ job, decryptedHeaders })),
map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })),
mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, logger)),
map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)),
map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)),
mergeMap((conditionalHeaders) => {
const urls = getFullUrls({ config, job });
const urls = getFullUrls(config, job);
const hashUrl = urls[0];
if (apmGetAssets) apmGetAssets.end();
@ -60,7 +58,6 @@ export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFac
content_type: 'image/png',
content: base64,
size: (base64 && base64.length) || 0,
warnings,
};
}),

View file

@ -19,9 +19,7 @@ import { metadata } from './metadata';
import { JobParamsPNG, TaskPayloadPNG } from './types';
export const getExportType = (): ExportTypeDefinition<
JobParamsPNG,
CreateJobFn<JobParamsPNG>,
TaskPayloadPNG,
RunTaskFn<TaskPayloadPNG>
> => ({
...metadata,

View file

@ -11,7 +11,7 @@ import { ReportingCore } from '../../../';
import { LevelLogger } from '../../../lib';
import { LayoutParams, PreserveLayout } from '../../../lib/layouts';
import { ScreenshotResults } from '../../../lib/screenshots';
import { ConditionalHeaders } from '../../../types';
import { ConditionalHeaders } from '../../common';
export async function generatePngObservableFactory(reporting: ReportingCore) {
const getScreenshots = await reporting.getScreenshotsObservable();
@ -19,7 +19,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
return function generatePngObservable(
logger: LevelLogger,
url: string,
browserTimezone: string,
browserTimezone: string | undefined,
conditionalHeaders: ConditionalHeaders,
layoutParams: LayoutParams
): Rx.Observable<{ base64: string | null; warnings: string[] }> {

View file

@ -4,19 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { BaseParams, BasePayload } from '../../../server/types';
import { LayoutParams } from '../../lib/layouts';
import { BaseParams, BasePayload } from '../../types';
interface BaseParamsPNG {
layout: LayoutParams;
forceNow?: string;
relativeUrl: string;
}
// Job params: structure of incoming user request data
export interface JobParamsPNG extends BaseParams {
title: string;
relativeUrl: string;
}
export type JobParamsPNG = BaseParamsPNG & BaseParams;
// Job payload: structure of stored job data provided by create_job
export interface TaskPayloadPNG extends BasePayload<JobParamsPNG> {
browserTimezone: string;
forceNow?: string;
layout: LayoutParams;
relativeUrl: string;
}
export type TaskPayloadPNG = BaseParamsPNG & BasePayload;

View file

@ -7,10 +7,11 @@
import { cryptoFactory } from '../../../lib';
import { CreateJobFn, CreateJobFnFactory } from '../../../types';
import { validateUrls } from '../../common';
import { JobParamsPDF } from '../types';
import { JobParamsPDF, TaskPayloadPDF } from '../types';
export const createJobFnFactory: CreateJobFnFactory<CreateJobFn<
JobParamsPDF
JobParamsPDF,
TaskPayloadPDF
>> = function createJobFactoryFn(reporting) {
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));

View file

@ -8,7 +8,8 @@ import apm from 'elastic-apm-node';
import * as Rx from 'rxjs';
import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators';
import { PDF_JOB_TYPE } from '../../../../common/constants';
import { RunTaskFn, RunTaskFnFactory, TaskRunResult } from '../../../types';
import { TaskRunResult } from '../../../lib/tasks';
import { RunTaskFn, RunTaskFnFactory } from '../../../types';
import {
decryptJobHeaders,
getConditionalHeaders,
@ -19,12 +20,9 @@ import { generatePdfObservableFactory } from '../lib/generate_pdf';
import { getCustomLogo } from '../lib/get_custom_logo';
import { TaskPayloadPDF } from '../types';
type QueuedPdfExecutorFactory = RunTaskFnFactory<RunTaskFn<TaskPayloadPDF>>;
export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFactoryFn(
reporting,
parentLogger
) {
export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<
TaskPayloadPDF
>> = function executeJobFactoryFn(reporting, parentLogger) {
const config = reporting.getConfig();
const encryptionKey = config.get('encryptionKey');
@ -39,12 +37,12 @@ export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFac
const jobLogger = logger.clone([jobId]);
const process$: Rx.Observable<TaskRunResult> = Rx.of(1).pipe(
mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })),
map((decryptedHeaders) => omitBlockedHeaders({ job, decryptedHeaders })),
map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })),
mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, logger)),
map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)),
map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)),
mergeMap((conditionalHeaders) => getCustomLogo(reporting, conditionalHeaders, job.spaceId)),
mergeMap(({ logo, conditionalHeaders }) => {
const urls = getFullUrls({ config, job });
const urls = getFullUrls(config, job);
const { browserTimezone, layout, title } = job;
if (apmGetAssets) apmGetAssets.end();

View file

@ -19,9 +19,7 @@ import { metadata } from './metadata';
import { JobParamsPDF, TaskPayloadPDF } from './types';
export const getExportType = (): ExportTypeDefinition<
JobParamsPDF,
CreateJobFn<JobParamsPDF>,
TaskPayloadPDF,
RunTaskFn<TaskPayloadPDF>
> => ({
...metadata,

View file

@ -9,9 +9,9 @@ import * as Rx from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { ReportingCore } from '../../../';
import { LevelLogger } from '../../../lib';
import { createLayout, LayoutInstance, LayoutParams } from '../../../lib/layouts';
import { createLayout, LayoutParams } from '../../../lib/layouts';
import { ScreenshotResults } from '../../../lib/screenshots';
import { ConditionalHeaders } from '../../../types';
import { ConditionalHeaders } from '../../common';
// @ts-ignore untyped module
import { pdf } from './pdf';
import { getTracker } from './tracker';
@ -35,7 +35,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
logger: LevelLogger,
title: string,
urls: string[],
browserTimezone: string,
browserTimezone: string | undefined,
conditionalHeaders: ConditionalHeaders,
layoutParams: LayoutParams,
logo?: string
@ -43,7 +43,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
const tracker = getTracker();
tracker.startLayout();
const layout = createLayout(captureConfig, layoutParams) as LayoutInstance;
const layout = createLayout(captureConfig, layoutParams);
tracker.endLayout();
tracker.startScreenshots();

View file

@ -11,7 +11,6 @@ import {
createMockReportingCore,
} from '../../../test_helpers';
import { getConditionalHeaders } from '../../common';
import { TaskPayloadPDF } from '../types';
import { getCustomLogo } from './get_custom_logo';
let mockConfig: ReportingConfig;
@ -39,11 +38,7 @@ test(`gets logo from uiSettings`, async () => {
get: mockGet,
});
const conditionalHeaders = getConditionalHeaders({
job: {} as TaskPayloadPDF,
filteredHeaders: permittedHeaders,
config: mockConfig,
});
const conditionalHeaders = getConditionalHeaders(mockConfig, permittedHeaders);
const { logo } = await getCustomLogo(mockReportingPlugin, conditionalHeaders);

View file

@ -6,7 +6,7 @@
import { ReportingCore } from '../../../';
import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants';
import { ConditionalHeaders } from '../../../types';
import { ConditionalHeaders } from '../../common';
export const getCustomLogo = async (
reporting: ReportingCore,

View file

@ -4,20 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { BaseParams, BasePayload } from '../../../server/types';
import { LayoutInstance, LayoutParams } from '../../lib/layouts';
import { LayoutParams } from '../../lib/layouts';
import { BaseParams, BasePayload } from '../../types';
interface BaseParamsPDF {
layout: LayoutParams;
forceNow?: string;
relativeUrls: string[];
}
// Job params: structure of incoming user request data, after being parsed from RISON
export interface JobParamsPDF extends BaseParams {
title: string;
relativeUrls: string[];
layout: LayoutInstance;
}
export type JobParamsPDF = BaseParamsPDF & BaseParams;
// Job payload: structure of stored job data provided by create_job
export interface TaskPayloadPDF extends BasePayload<JobParamsPDF> {
browserTimezone: string;
forceNow?: string;
layout: LayoutParams;
relativeUrls: string[];
}
export type TaskPayloadPDF = BaseParamsPDF & BasePayload;

View file

@ -24,9 +24,7 @@ const messages = {
},
};
const makeManagementFeature = (
exportTypes: Array<ExportTypeDefinition<unknown, unknown, unknown, unknown>>
) => {
const makeManagementFeature = (exportTypes: ExportTypeDefinition[]) => {
return {
id: 'management',
checkLicense: (license?: ILicense) => {
@ -59,9 +57,7 @@ const makeManagementFeature = (
};
};
const makeExportTypeFeature = (
exportType: ExportTypeDefinition<unknown, unknown, unknown, unknown>
) => {
const makeExportTypeFeature = (exportType: ExportTypeDefinition) => {
return {
id: exportType.id,
checkLicense: (license?: ILicense) => {

View file

@ -5,13 +5,13 @@
*/
import { ReportingCore } from '../core';
import { JobSource, TaskRunResult } from '../types';
import { createWorkerFactory } from './create_worker';
// @ts-ignore
import { Esqueue } from './esqueue';
import { createTaggedLogger } from './esqueue/create_tagged_logger';
import { LevelLogger } from './level_logger';
import { ReportingStore } from './store';
import { ReportDocument, ReportingStore } from './store';
import { TaskRunResult } from './tasks';
interface ESQueueWorker {
on: (event: string, handler: any) => void;
@ -32,7 +32,7 @@ export interface ESQueueInstance {
// GenericWorkerFn is a generic for ImmediateExecuteFn<JobParamsType> | ESQueueWorkerExecuteFn<ScheduledTaskParamsType>,
type GenericWorkerFn<JobParamsType> = (
jobSource: JobSource<JobParamsType>,
jobSource: ReportDocument,
...workerRestArgs: any[]
) => void | Promise<TaskRunResult>;

View file

@ -9,10 +9,12 @@ import { PLUGIN_ID } from '../../common/constants';
import { durationToNumber } from '../../common/schema_utils';
import { ReportingCore } from '../../server';
import { LevelLogger } from '../../server/lib';
import { ExportTypeDefinition, JobSource, RunTaskFn } from '../../server/types';
import { RunTaskFn } from '../../server/types';
import { ESQueueInstance } from './create_queue';
// @ts-ignore untyped dependency
import { events as esqueueEvents } from './esqueue';
import { ReportDocument } from './store';
import { ReportTaskParams } from './tasks';
export function createWorkerFactory<JobParamsType>(reporting: ReportingCore, logger: LevelLogger) {
const config = reporting.getConfig();
@ -23,18 +25,16 @@ export function createWorkerFactory<JobParamsType>(reporting: ReportingCore, log
// Once more document types are added, this will need to be passed in
return async function createWorker(queue: ESQueueInstance) {
// export type / execute job map
const jobExecutors: Map<string, RunTaskFn<unknown>> = new Map();
const jobExecutors: Map<string, RunTaskFn> = new Map();
for (const exportType of reporting.getExportTypesRegistry().getAll() as Array<
ExportTypeDefinition<JobParamsType, unknown, unknown, RunTaskFn<unknown>>
>) {
for (const exportType of reporting.getExportTypesRegistry().getAll()) {
const jobExecutor = exportType.runTaskFnFactory(reporting, logger);
jobExecutors.set(exportType.jobType, jobExecutor);
}
const workerFn = <TaskPayloadType>(
jobSource: JobSource<TaskPayloadType>,
jobParams: TaskPayloadType,
const workerFn = (
jobSource: ReportDocument,
payload: ReportTaskParams['payload'],
cancellationToken: CancellationToken
) => {
const {
@ -52,7 +52,7 @@ export function createWorkerFactory<JobParamsType>(reporting: ReportingCore, log
}
// pass the work to the jobExecutor
return jobTypeExecutor(jobId, jobParams, cancellationToken);
return jobTypeExecutor(jobId, payload, cancellationToken);
};
const workerOptions = {

View file

@ -6,7 +6,8 @@
import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
import { ReportingCore } from '../';
import { BaseParams, CreateJobFn, ReportingUser } from '../types';
import { durationToNumber } from '../../common/schema_utils';
import { BaseParams, ReportingUser } from '../types';
import { LevelLogger } from './';
import { Report } from './store';
@ -23,6 +24,13 @@ export function enqueueJobFactory(
parentLogger: LevelLogger
): EnqueueJobFn {
const logger = parentLogger.clone(['queue-job']);
const config = reporting.getConfig();
const jobSettings = {
timeout: durationToNumber(config.get('queue', 'timeout')),
browser_type: config.get('capture', 'browser', 'type'),
max_attempts: config.get('capture', 'maxAttempts'),
priority: 10, // unused
};
return async function enqueueJob(
exportTypeId: string,
@ -31,8 +39,6 @@ export function enqueueJobFactory(
context: RequestHandlerContext,
request: KibanaRequest
) {
type CreateJobFnType = CreateJobFn<BaseParams>;
const exportType = reporting.getExportTypesRegistry().getById(exportTypeId);
if (exportType == null) {
@ -40,15 +46,24 @@ export function enqueueJobFactory(
}
const [createJob, { store }] = await Promise.all([
exportType.createJobFnFactory(reporting, logger) as CreateJobFnType,
exportType.createJobFnFactory(reporting, logger),
reporting.getPluginStartDeps(),
]);
// add encrytped headers
const payload = await createJob(jobParams, context, request);
const job = await createJob(jobParams, context, request);
const pendingReport = new Report({
jobtype: exportType.jobType,
created_by: user ? user.username : false,
payload: job,
meta: {
objectType: jobParams.objectType,
layout: jobParams.layout?.id,
},
...jobSettings,
});
// store the pending report, puts it in the Reporting Management UI table
const report = await store.addReport(exportType.jobType, user, payload);
const report = await store.addReport(pendingReport);
logger.info(`Scheduled ${exportType.name} report: ${report._id}`);

View file

@ -9,21 +9,16 @@ import { getExportType as getTypeCsv } from '../export_types/csv';
import { getExportType as getTypeCsvFromSavedObject } from '../export_types/csv_from_savedobject';
import { getExportType as getTypePng } from '../export_types/png';
import { getExportType as getTypePrintablePdf } from '../export_types/printable_pdf';
import { ExportTypeDefinition } from '../types';
import { CreateJobFn, ExportTypeDefinition } from '../types';
type GetCallbackFn<JobParamsType, CreateJobFnType, JobPayloadType, RunTaskFnType> = (
item: ExportTypeDefinition<JobParamsType, CreateJobFnType, JobPayloadType, CreateJobFnType>
) => boolean;
// => ExportTypeDefinition<T, U, V, W>
type GetCallbackFn = (item: ExportTypeDefinition) => boolean;
export class ExportTypesRegistry {
private _map: Map<string, ExportTypeDefinition<any, any, any, any>> = new Map();
private _map: Map<string, ExportTypeDefinition> = new Map();
constructor() {}
register<JobParamsType, CreateJobFnType, JobPayloadType, RunTaskFnType>(
item: ExportTypeDefinition<JobParamsType, CreateJobFnType, JobPayloadType, RunTaskFnType>
): void {
register(item: ExportTypeDefinition): void {
if (!isString(item.id)) {
throw new Error(`'item' must have a String 'id' property `);
}
@ -43,35 +38,21 @@ export class ExportTypesRegistry {
return this._map.size;
}
getById<JobParamsType, CreateJobFnType, JobPayloadType, RunTaskFnType>(
id: string
): ExportTypeDefinition<JobParamsType, CreateJobFnType, JobPayloadType, RunTaskFnType> {
getById(id: string): ExportTypeDefinition {
if (!this._map.has(id)) {
throw new Error(`Unknown id ${id}`);
}
return this._map.get(id) as ExportTypeDefinition<
JobParamsType,
CreateJobFnType,
JobPayloadType,
RunTaskFnType
>;
return this._map.get(id) as ExportTypeDefinition;
}
get<JobParamsType, CreateJobFnType, JobPayloadType, RunTaskFnType>(
findType: GetCallbackFn<JobParamsType, CreateJobFnType, JobPayloadType, RunTaskFnType>
): ExportTypeDefinition<JobParamsType, CreateJobFnType, JobPayloadType, RunTaskFnType> {
get(findType: GetCallbackFn): ExportTypeDefinition {
let result;
for (const value of this._map.values()) {
if (!findType(value)) {
continue; // try next value
}
const foundResult: ExportTypeDefinition<
JobParamsType,
CreateJobFnType,
JobPayloadType,
RunTaskFnType
> = value;
const foundResult: ExportTypeDefinition = value;
if (result) {
throw new Error('Found multiple items matching predicate.');
@ -88,13 +69,19 @@ export class ExportTypesRegistry {
}
}
// TODO: Define a 2nd ExportTypeRegistry instance for "immediate execute" report job types only.
// It should not require a `CreateJobFn` for its ExportTypeDefinitions, which only makes sense for async.
// Once that is done, the `any` types below can be removed.
/*
* @return ExportTypeRegistry: the ExportTypeRegistry instance that should be
* used to register async export type definitions
*/
export function getExportTypesRegistry(): ExportTypesRegistry {
const registry = new ExportTypesRegistry();
/* this replaces the previously async method of registering export types,
* where this would run a directory scan and types would be registered via
* discovery */
const getTypeFns: Array<() => ExportTypeDefinition<any, any, any, any>> = [
type CreateFnType = CreateJobFn<any, any>; // can not specify params types because different type of params are not assignable to each other
type RunFnType = any; // can not specify because ImmediateExecuteFn is not assignable to RunTaskFn
const getTypeFns: Array<() => ExportTypeDefinition<CreateFnType, RunFnType>> = [
getTypeCsv,
getTypeCsvFromSavedObject,
getTypePng,

View file

@ -6,12 +6,11 @@
import { CaptureConfig } from '../../types';
import { LayoutParams, LayoutTypes } from './';
import { Layout } from './layout';
import { PreserveLayout } from './preserve_layout';
import { PrintLayout } from './print_layout';
export function createLayout(captureConfig: CaptureConfig, layoutParams?: LayoutParams): Layout {
if (layoutParams && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) {
export function createLayout(captureConfig: CaptureConfig, layoutParams?: LayoutParams) {
if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) {
return new PreserveLayout(layoutParams.dimensions);
}

View file

@ -53,7 +53,7 @@ export interface Size {
export interface LayoutParams {
id: string;
dimensions: Size;
dimensions?: Size;
selectors?: LayoutSelectorDictionary;
}
@ -64,4 +64,4 @@ interface LayoutSelectors {
positionElements?: (browser: HeadlessChromiumDriver, logger: LevelLogger) => Promise<void>;
}
export type LayoutInstance = Layout & LayoutSelectors & Size;
export type LayoutInstance = Layout & LayoutSelectors & Partial<Size>;

View file

@ -12,12 +12,13 @@ import {
LayoutTypes,
PageSizeParams,
Size,
LayoutInstance,
} from './';
// We use a zoom of two to bump up the resolution of the screenshot a bit.
const ZOOM: number = 2;
export class PreserveLayout extends Layout {
export class PreserveLayout extends Layout implements LayoutInstance {
public readonly selectors: LayoutSelectorDictionary = getDefaultLayoutSelectors();
public readonly groupCount = 1;
public readonly height: number;

View file

@ -9,10 +9,16 @@ import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer';
import { LevelLogger } from '../';
import { HeadlessChromiumDriver } from '../../browsers';
import { CaptureConfig } from '../../types';
import { getDefaultLayoutSelectors, LayoutSelectorDictionary, LayoutTypes, Size } from './';
import {
getDefaultLayoutSelectors,
LayoutInstance,
LayoutSelectorDictionary,
LayoutTypes,
Size,
} from './';
import { Layout } from './layout';
export class PrintLayout extends Layout {
export class PrintLayout extends Layout implements LayoutInstance {
public readonly selectors: LayoutSelectorDictionary = {
...getDefaultLayoutSelectors(),
screenshot: '[data-shared-item]',

View file

@ -5,7 +5,7 @@
*/
import { LevelLogger, startTrace } from '../';
import { LayoutInstance } from '../../../common/types';
import { LayoutInstance } from '../layouts';
import { HeadlessChromiumDriver } from '../../browsers';
import { CONTEXT_GETTIMERANGE } from './constants';

View file

@ -6,7 +6,7 @@
import * as Rx from 'rxjs';
import { LevelLogger } from '../';
import { ConditionalHeaders } from '../../types';
import { ConditionalHeaders } from '../../export_types/common';
import { LayoutInstance } from '../layouts';
export { screenshotsObservableFactory } from './observable';
@ -16,7 +16,7 @@ export interface ScreenshotObservableOpts {
urls: string[];
conditionalHeaders: ConditionalHeaders;
layout: LayoutInstance;
browserTimezone: string;
browserTimezone?: string;
}
export interface AttributesMap {

View file

@ -18,6 +18,7 @@ jest.mock('../../browsers/chromium/puppeteer', () => ({
import moment from 'moment';
import * as Rx from 'rxjs';
import { HeadlessChromiumDriver } from '../../browsers';
import { ConditionalHeaders } from '../../export_types/common';
import {
createMockBrowserDriverFactory,
createMockConfig,
@ -25,7 +26,6 @@ import {
createMockLayoutInstance,
createMockLevelLogger,
} from '../../test_helpers';
import { ConditionalHeaders } from '../../types';
import { ElementsPositionAndAttribute } from './';
import * as contexts from './constants';
import { screenshotsObservableFactory } from './observable';

View file

@ -5,10 +5,11 @@
*/
import { i18n } from '@kbn/i18n';
import { durationToNumber } from '../../../common/schema_utils';
import { LevelLogger, startTrace } from '../';
import { durationToNumber } from '../../../common/schema_utils';
import { HeadlessChromiumDriver } from '../../browsers';
import { CaptureConfig, ConditionalHeaders } from '../../types';
import { ConditionalHeaders } from '../../export_types/common';
import { CaptureConfig } from '../../types';
export const openUrl = async (
captureConfig: CaptureConfig,

View file

@ -4,5 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { Report } from './report';
export { Report, ReportDocument } from './report';
export { ReportingStore } from './store';

View file

@ -14,7 +14,8 @@ describe('Class Report', () => {
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
max_attempts: 50,
payload: { headers: 'payload_test_field', objectType: 'testOt' },
payload: { headers: 'payload_test_field', objectType: 'testOt', title: 'cool report' },
meta: { objectType: 'test' },
timeout: 30000,
priority: 1,
});
@ -25,11 +26,10 @@ describe('Class Report', () => {
attempts: 0,
browser_type: 'browser_type_test_string',
completed_at: undefined,
created_at: undefined,
created_by: 'created_by_test_string',
jobtype: 'test-report',
max_attempts: 50,
meta: undefined,
meta: { objectType: 'test' },
payload: { headers: 'payload_test_field', objectType: 'testOt' },
priority: 1,
started_at: undefined,
@ -38,12 +38,16 @@ describe('Class Report', () => {
},
});
expect(report.toApiJSON()).toMatchObject({
attempts: 0,
browser_type: 'browser_type_test_string',
created_by: 'created_by_test_string',
index: '.reporting-test-index-12345',
jobtype: 'test-report',
max_attempts: 50,
payload: { headers: 'payload_test_field', objectType: 'testOt' },
meta: { objectType: 'test' },
priority: 1,
status: 'pending',
timeout: 30000,
});
@ -57,7 +61,8 @@ describe('Class Report', () => {
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
max_attempts: 50,
payload: { headers: 'payload_test_field', objectType: 'testOt' },
payload: { headers: 'payload_test_field', objectType: 'testOt', title: 'hot report' },
meta: { objectType: 'stange' },
timeout: 30000,
priority: 1,
});
@ -70,51 +75,46 @@ describe('Class Report', () => {
};
report.updateWithEsDoc(metadata);
expect(report.toEsDocsJSON()).toMatchInlineSnapshot(`
Object {
"_id": "12342p9o387549o2345",
"_index": ".reporting-test-update",
"_source": Object {
"attempts": 0,
"browser_type": "browser_type_test_string",
"completed_at": undefined,
"created_at": undefined,
"created_by": "created_by_test_string",
"jobtype": "test-report",
"max_attempts": 50,
"meta": undefined,
"payload": Object {
"headers": "payload_test_field",
"objectType": "testOt",
},
"priority": 1,
"started_at": undefined,
"status": "pending",
"timeout": 30000,
},
}
`);
expect(report.toApiJSON()).toMatchInlineSnapshot(`
Object {
"attempts": 0,
"browser_type": "browser_type_test_string",
"completed_at": undefined,
"created_at": undefined,
"created_by": "created_by_test_string",
"id": "12342p9o387549o2345",
"index": ".reporting-test-update",
"jobtype": "test-report",
"max_attempts": 50,
"meta": undefined,
"payload": Object {
"headers": "payload_test_field",
"objectType": "testOt",
},
"priority": 1,
"started_at": undefined,
"status": "pending",
"timeout": 30000,
}
`);
expect(report.toEsDocsJSON()).toMatchObject({
_id: '12342p9o387549o2345',
_index: '.reporting-test-update',
_source: {
attempts: 0,
browser_type: 'browser_type_test_string',
completed_at: undefined,
created_by: 'created_by_test_string',
jobtype: 'test-report',
max_attempts: 50,
meta: { objectType: 'stange' },
payload: { objectType: 'testOt' },
priority: 1,
started_at: undefined,
status: 'pending',
timeout: 30000,
},
});
expect(report.toApiJSON()).toMatchObject({
attempts: 0,
browser_type: 'browser_type_test_string',
completed_at: undefined,
created_by: 'created_by_test_string',
id: '12342p9o387549o2345',
index: '.reporting-test-update',
jobtype: 'test-report',
max_attempts: 50,
meta: { objectType: 'stange' },
payload: { headers: 'payload_test_field', objectType: 'testOt' },
priority: 1,
started_at: undefined,
status: 'pending',
timeout: 30000,
});
});
it('throws error if converted to task JSON before being synced with ES storage', () => {
const report = new Report({} as any);
expect(() => report.updateWithEsDoc(report)).toThrowErrorMatchingInlineSnapshot(
`"Report object from ES has missing fields!"`
);
});
});

View file

@ -4,84 +4,96 @@
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment';
// @ts-ignore no module definition
import Puid from 'puid';
import { JobStatus, ReportApiJSON } from '../../../common/types';
import { JobStatuses } from '../../../constants';
import { LayoutInstance } from '../layouts';
import { LayoutParams } from '../layouts';
import { TaskRunResult } from '../tasks';
/*
* The document created by Reporting to store in the .reporting index
*/
interface ReportingDocument {
interface ReportDocumentHead {
_id: string;
_index: string;
_seq_no: unknown;
_primary_term: unknown;
}
/*
* The document created by Reporting to store in the .reporting index
*/
export interface ReportDocument extends ReportDocumentHead {
_source: ReportSource;
}
export interface ReportSource {
jobtype: string;
kibana_name: string;
kibana_id: string;
created_by: string | false;
payload: {
headers: string; // encrypted headers
browserTimezone?: string; // may use timezone from advanced settings
objectType: string;
layout?: LayoutInstance;
title: string;
layout?: LayoutParams;
};
meta: unknown;
meta: { objectType: string; layout?: string };
browser_type: string;
max_attempts: number;
timeout: number;
status: string;
status: JobStatus;
attempts: number;
output?: unknown;
output: TaskRunResult | null;
started_at?: string;
completed_at?: string;
created_at?: string;
created_at: string;
priority?: number;
process_expiration?: string;
}
/*
* The document created by Reporting to store as task parameters for Task
* Manager to reference the report in .reporting
*/
const puid = new Puid();
export class Report implements Partial<ReportingDocument> {
export class Report implements Partial<ReportSource> {
public _index?: string;
public _id: string;
public _primary_term?: unknown; // set by ES
public _seq_no: unknown; // set by ES
public readonly jobtype: string;
public readonly created_at?: string;
public readonly created_by?: string | false;
public readonly payload: {
headers: string; // encrypted headers
objectType: string;
layout?: LayoutInstance;
};
public readonly meta: unknown;
public readonly max_attempts: number;
public readonly browser_type?: string;
public readonly kibana_name: ReportSource['kibana_name'];
public readonly kibana_id: ReportSource['kibana_id'];
public readonly jobtype: ReportSource['jobtype'];
public readonly created_at: ReportSource['created_at'];
public readonly created_by: ReportSource['created_by'];
public readonly payload: ReportSource['payload'];
public readonly status: string;
public readonly attempts: number;
public readonly output?: unknown;
public readonly started_at?: string;
public readonly completed_at?: string;
public readonly process_expiration?: string;
public readonly priority?: number;
public readonly timeout?: number;
public readonly meta: ReportSource['meta'];
public readonly max_attempts: ReportSource['max_attempts'];
public readonly browser_type?: ReportSource['browser_type'];
public readonly status: ReportSource['status'];
public readonly attempts: ReportSource['attempts'];
public readonly output?: ReportSource['output'];
public readonly started_at?: ReportSource['started_at'];
public readonly completed_at?: ReportSource['completed_at'];
public readonly process_expiration?: ReportSource['process_expiration'];
public readonly priority?: ReportSource['priority'];
public readonly timeout?: ReportSource['timeout'];
/*
* Create an unsaved report
* Index string is required
*/
constructor(opts: Partial<ReportingDocument>) {
constructor(opts: Partial<ReportSource> & Partial<ReportDocumentHead>) {
this._id = opts._id != null ? opts._id : puid.generate();
this._index = opts._index;
this._primary_term = opts._primary_term;
this._seq_no = opts._seq_no;
this.payload = opts.payload!;
this.kibana_name = opts.kibana_name!;
this.kibana_id = opts.kibana_id!;
this.jobtype = opts.jobtype!;
this.max_attempts = opts.max_attempts!;
this.attempts = opts.attempts || 0;
@ -89,9 +101,9 @@ export class Report implements Partial<ReportingDocument> {
this.process_expiration = opts.process_expiration;
this.timeout = opts.timeout;
this.created_at = opts.created_at;
this.created_by = opts.created_by;
this.meta = opts.meta;
this.created_at = opts.created_at || moment.utc().toISOString();
this.created_by = opts.created_by || false;
this.meta = opts.meta || { objectType: 'unknown' };
this.browser_type = opts.browser_type;
this.priority = opts.priority;
@ -141,10 +153,12 @@ export class Report implements Partial<ReportingDocument> {
/*
* Data structure for API responses
*/
toApiJSON() {
toApiJSON(): ReportApiJSON {
return {
id: this._id,
index: this._index,
index: this._index!,
kibana_name: this.kibana_name,
kibana_id: this.kibana_id,
jobtype: this.jobtype,
created_at: this.created_at,
created_by: this.created_by,

View file

@ -48,28 +48,25 @@ describe('ReportingStore', () => {
describe('addReport', () => {
it('returns Report object', async () => {
const store = new ReportingStore(mockCore, mockLogger);
const reportType = 'unknowntype';
const reportPayload = {
browserTimezone: 'UTC',
headers: 'rp_headers_1',
objectType: 'testOt',
};
await expect(
store.addReport(reportType, { username: 'username1' }, reportPayload)
).resolves.toMatchObject({
const mockReport = new Report({
_index: '.reporting-mock',
attempts: 0,
created_by: 'username1',
jobtype: 'unknowntype',
status: 'pending',
payload: {},
meta: {},
} as any);
await expect(store.addReport(mockReport)).resolves.toMatchObject({
_primary_term: undefined,
_seq_no: undefined,
attempts: 0,
browser_type: undefined,
completed_at: undefined,
created_by: 'username1',
jobtype: 'unknowntype',
max_attempts: undefined,
payload: {},
priority: 10,
started_at: undefined,
meta: {},
status: 'pending',
timeout: 120000,
});
});
@ -83,15 +80,15 @@ describe('ReportingStore', () => {
mockCore = await createMockReportingCore(mockConfig);
const store = new ReportingStore(mockCore, mockLogger);
const reportType = 'unknowntype';
const reportPayload = {
browserTimezone: 'UTC',
headers: 'rp_headers_2',
objectType: 'testOt',
};
expect(
store.addReport(reportType, { username: 'user1' }, reportPayload)
).rejects.toMatchInlineSnapshot(`[Error: Invalid index interval: centurially]`);
const mockReport = new Report({
_index: '.reporting-errortest',
jobtype: 'unknowntype',
payload: {},
meta: {},
} as any);
expect(store.addReport(mockReport)).rejects.toMatchInlineSnapshot(
`[TypeError: this.client.callAsInternalUser is not a function]`
);
});
it('handles error creating the index', async () => {
@ -100,15 +97,15 @@ describe('ReportingStore', () => {
callClusterStub.withArgs('indices.create').rejects(new Error('horrible error'));
const store = new ReportingStore(mockCore, mockLogger);
const reportType = 'unknowntype';
const reportPayload = {
browserTimezone: 'UTC',
headers: 'rp_headers_3',
objectType: 'testOt',
};
await expect(
store.addReport(reportType, { username: 'user1' }, reportPayload)
).rejects.toMatchInlineSnapshot(`[Error: horrible error]`);
const mockReport = new Report({
_index: '.reporting-errortest',
jobtype: 'unknowntype',
payload: {},
meta: {},
} as any);
await expect(store.addReport(mockReport)).rejects.toMatchInlineSnapshot(
`[Error: horrible error]`
);
});
/* Creating the index will fail, if there were multiple jobs staged in
@ -123,15 +120,15 @@ describe('ReportingStore', () => {
callClusterStub.withArgs('indices.create').rejects(new Error('devastating error'));
const store = new ReportingStore(mockCore, mockLogger);
const reportType = 'unknowntype';
const reportPayload = {
browserTimezone: 'UTC',
headers: 'rp_headers_4',
objectType: 'testOt',
};
await expect(
store.addReport(reportType, { username: 'user1' }, reportPayload)
).rejects.toMatchInlineSnapshot(`[Error: devastating error]`);
const mockReport = new Report({
_index: '.reporting-mock',
jobtype: 'unknowntype',
payload: {},
meta: {},
} as any);
await expect(store.addReport(mockReport)).rejects.toMatchInlineSnapshot(
`[Error: devastating error]`
);
});
it('skips creating the index if already exists', async () => {
@ -142,28 +139,20 @@ describe('ReportingStore', () => {
.rejects(new Error('resource_already_exists_exception')); // will be triggered but ignored
const store = new ReportingStore(mockCore, mockLogger);
const reportType = 'unknowntype';
const reportPayload = {
browserTimezone: 'UTC',
headers: 'rp_headers_5',
objectType: 'testOt',
};
await expect(
store.addReport(reportType, { username: 'user1' }, reportPayload)
).resolves.toMatchObject({
const mockReport = new Report({
created_by: 'user1',
jobtype: 'unknowntype',
payload: {},
meta: {},
} as any);
await expect(store.addReport(mockReport)).resolves.toMatchObject({
_primary_term: undefined,
_seq_no: undefined,
attempts: 0,
browser_type: undefined,
completed_at: undefined,
created_by: 'user1',
jobtype: 'unknowntype',
max_attempts: undefined,
payload: {},
priority: 10,
started_at: undefined,
status: 'pending',
timeout: 120000,
});
});
@ -175,26 +164,24 @@ describe('ReportingStore', () => {
.rejects(new Error('resource_already_exists_exception')); // will be triggered but ignored
const store = new ReportingStore(mockCore, mockLogger);
const reportType = 'unknowntype';
const reportPayload = {
browserTimezone: 'UTC',
headers: 'rp_test_headers',
objectType: 'testOt',
};
await expect(store.addReport(reportType, false, reportPayload)).resolves.toMatchObject({
const mockReport = new Report({
_index: '.reporting-unsecured',
attempts: 0,
created_by: false,
jobtype: 'unknowntype',
payload: {},
meta: {},
status: 'pending',
} as any);
await expect(store.addReport(mockReport)).resolves.toMatchObject({
_primary_term: undefined,
_seq_no: undefined,
attempts: 0,
browser_type: undefined,
completed_at: undefined,
created_by: false,
jobtype: 'unknowntype',
max_attempts: undefined,
meta: {},
payload: {},
priority: 10,
started_at: undefined,
status: 'pending',
timeout: 120000,
});
});
});
@ -209,8 +196,10 @@ describe('ReportingStore', () => {
browser_type: 'browser_type_test_string',
max_attempts: 50,
payload: {
title: 'test report',
headers: 'rp_test_headers',
objectType: 'testOt',
browserTimezone: 'ABC',
},
timeout: 30000,
priority: 1,
@ -248,8 +237,10 @@ describe('ReportingStore', () => {
browser_type: 'browser_type_test_string',
max_attempts: 50,
payload: {
title: 'test report',
headers: 'rp_test_headers',
objectType: 'testOt',
browserTimezone: 'BCD',
},
timeout: 30000,
priority: 1,
@ -287,8 +278,10 @@ describe('ReportingStore', () => {
browser_type: 'browser_type_test_string',
max_attempts: 50,
payload: {
title: 'test report',
headers: 'rp_test_headers',
objectType: 'testOt',
browserTimezone: 'CDE',
},
timeout: 30000,
priority: 1,
@ -326,8 +319,10 @@ describe('ReportingStore', () => {
browser_type: 'browser_type_test_string',
max_attempts: 50,
payload: {
title: 'test report',
headers: 'rp_test_headers',
objectType: 'testOt',
browserTimezone: 'utc',
},
timeout: 30000,
priority: 1,

View file

@ -5,21 +5,12 @@
*/
import { ElasticsearchServiceSetup } from 'src/core/server';
import { durationToNumber } from '../../../common/schema_utils';
import { LevelLogger, statuses } from '../';
import { ReportingCore } from '../../';
import { BaseParams, BaseParamsEncryptedFields, ReportingUser } from '../../types';
import { indexTimestamp } from './index_timestamp';
import { mapping } from './mapping';
import { Report } from './report';
interface JobSettings {
timeout: number;
browser_type: string;
max_attempts: number;
priority: number;
}
const checkReportIsEditable = (report: Report) => {
if (!report._id || !report._index) {
throw new Error(`Report object is not synced with ES!`);
@ -35,7 +26,6 @@ const checkReportIsEditable = (report: Report) => {
export class ReportingStore {
private readonly indexPrefix: string;
private readonly indexInterval: string;
private readonly jobSettings: JobSettings;
private client: ElasticsearchServiceSetup['legacy']['client'];
private logger: LevelLogger;
@ -46,13 +36,6 @@ export class ReportingStore {
this.client = elasticsearch.legacy.client;
this.indexPrefix = config.get('index');
this.indexInterval = config.get('queue', 'indexInterval');
this.jobSettings = {
timeout: durationToNumber(config.get('queue', 'timeout')),
browser_type: config.get('capture', 'browser', 'type'),
max_attempts: config.get('capture', 'maxAttempts'),
priority: 10, // unused
};
this.logger = logger;
}
@ -101,36 +84,17 @@ export class ReportingStore {
* Called from addReport, which handles any errors
*/
private async indexReport(report: Report) {
const params = report.payload;
// Queing is handled by TM. These queueing-based fields for reference in Report Info panel
const infoFields = {
timeout: report.timeout,
process_expiration: new Date(0), // use epoch so the job query works
created_at: new Date(),
attempts: 0,
max_attempts: report.max_attempts,
status: statuses.JOB_STATUS_PENDING,
browser_type: report.browser_type,
};
const indexParams = {
const doc = {
index: report._index,
id: report._id,
body: {
...infoFields,
jobtype: report.jobtype,
meta: {
// We are copying these values out of payload because these fields are indexed and can be aggregated on
// for tracking stats, while payload contents are not.
objectType: params.objectType,
layout: params.layout ? params.layout.id : 'none',
},
payload: report.payload,
created_by: report.created_by,
...report.toEsDocsJSON()._source,
process_expiration: new Date(0), // use epoch so the job query works
attempts: 0,
status: statuses.JOB_STATUS_PENDING,
},
};
return await this.client.callAsInternalUser('index', indexParams);
return await this.client.callAsInternalUser('index', doc);
}
/*
@ -140,23 +104,15 @@ export class ReportingStore {
return await this.client.callAsInternalUser('indices.refresh', { index });
}
public async addReport(
type: string,
user: ReportingUser,
payload: BaseParams & BaseParamsEncryptedFields
): Promise<Report> {
const timestamp = indexTimestamp(this.indexInterval);
const index = `${this.indexPrefix}-${timestamp}`;
public async addReport(report: Report): Promise<Report> {
let index = report._index;
if (!index) {
const timestamp = indexTimestamp(this.indexInterval);
index = `${this.indexPrefix}-${timestamp}`;
report._index = index;
}
await this.createIndex(index);
const report = new Report({
_index: index,
payload,
jobtype: type,
created_by: user ? user.username : false,
...this.jobSettings,
});
try {
const doc = await this.indexReport(report);
report.updateWithEsDoc(doc);
@ -166,7 +122,7 @@ export class ReportingStore {
return report;
} catch (err) {
this.logger.error(`Error in addReport!`);
this.logger.error(`Error in adding a report!`);
this.logger.error(err);
throw err;
}
@ -220,7 +176,7 @@ export class ReportingStore {
public async setReportCompleted(report: Report, stats: Partial<Report>): Promise<Report> {
try {
const { output } = stats as { output: any };
const { output } = stats;
const status =
output && output.warnings && output.warnings.length > 0
? statuses.JOB_STATUS_WARNINGS

View 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 { BasePayload } from '../../types';
import { ReportSource } from '../store/report';
/*
* The document created by Reporting to store as task parameters for Task
* Manager to reference the report in .reporting
*/
export interface ReportTaskParams<JobPayloadType = BasePayload> {
id: string;
index?: string; // For ad-hoc, which as an existing "pending" record
payload: JobPayloadType;
created_at: ReportSource['created_at'];
created_by: ReportSource['created_by'];
jobtype: ReportSource['jobtype'];
attempts: ReportSource['attempts'];
meta: ReportSource['meta'];
}
export interface TaskRunResult {
content_type: string | null;
content: string | null;
csv_contains_formulas?: boolean;
size: number;
max_size_reached?: boolean;
warnings?: string[];
}

View file

@ -9,8 +9,8 @@ import { ReportingCore } from '../..';
import { API_DIAGNOSE_URL } from '../../../common/constants';
import { browserStartLogs } from '../../browsers/chromium/driver_factory/start_logs';
import { LevelLogger as Logger } from '../../lib';
import { DiagnosticResponse } from '../../types';
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
import { DiagnosticResponse } from './';
const logsToHelpMap = {
'error while loading shared libraries': i18n.translate(

View file

@ -10,8 +10,8 @@ import { defaults, get } from 'lodash';
import { ReportingCore } from '../..';
import { API_DIAGNOSE_URL } from '../../../common/constants';
import { LevelLogger as Logger } from '../../lib';
import { DiagnosticResponse } from '../../types';
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
import { DiagnosticResponse } from './';
const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes';
const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length';

View file

@ -15,3 +15,9 @@ export const registerDiagnosticRoutes = (reporting: ReportingCore, logger: Logge
registerDiagnoseConfig(reporting, logger);
registerDiagnoseScreenshot(reporting, logger);
};
export interface DiagnosticResponse {
help: string[];
success: boolean;
logs: string;
}

View file

@ -11,8 +11,8 @@ import { omitBlockedHeaders } from '../../export_types/common';
import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url';
import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png';
import { LevelLogger as Logger } from '../../lib';
import { DiagnosticResponse } from '../../types';
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
import { DiagnosticResponse } from './';
export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Logger) => {
const setupDeps = reporting.getPluginSetupDeps();
@ -54,10 +54,7 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log
};
const headers = {
headers: omitBlockedHeaders({
job: null,
decryptedHeaders,
}),
headers: omitBlockedHeaders(decryptedHeaders),
conditions: {
hostname,
port: +port,

View file

@ -10,17 +10,20 @@ import { ReportingCore } from '../';
import { API_BASE_GENERATE_V1 } from '../../common/constants';
import { createJobFnFactory } from '../export_types/csv_from_savedobject/create_job';
import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job';
import { JobParamsPostPayloadPanelCsv } from '../export_types/csv_from_savedobject/types';
import {
JobParamsPanelCsv,
JobParamsPanelCsvPost,
} from '../export_types/csv_from_savedobject/types';
import { LevelLogger as Logger } from '../lib';
import { TaskRunResult } from '../types';
import { TaskRunResult } from '../lib/tasks';
import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing';
import { getJobParamsFromRequest } from './lib/get_job_params_from_request';
import { HandlerErrorFunction } from './types';
export type CsvFromSavedObjectRequest = KibanaRequest<
{ savedObjectType: string; savedObjectId: string },
JobParamsPanelCsv,
unknown,
JobParamsPostPayloadPanelCsv
JobParamsPanelCsvPost
>;
/*
@ -66,27 +69,22 @@ export function registerGenerateCsvFromSavedObjectImmediate(
},
userHandler(async (user, context, req: CsvFromSavedObjectRequest, res) => {
const logger = parentLogger.clone(['savedobject-csv']);
const jobParams = getJobParamsFromRequest(req, { isImmediate: true });
const jobParams = getJobParamsFromRequest(req);
const createJob = createJobFnFactory(reporting, logger);
const runTaskFn = runTaskFnFactory(reporting, logger);
try {
// FIXME: no create job for immediate download
const jobDocPayload = await createJob(jobParams, req.headers, context, req);
const payload = await createJob(jobParams, context, req);
const {
content_type: jobOutputContentType,
content: jobOutputContent,
size: jobOutputSize,
}: TaskRunResult = await runTaskFn(null, jobDocPayload, context, req);
}: TaskRunResult = await runTaskFn(null, payload, context, req);
logger.info(`Job output size: ${jobOutputSize} bytes`);
/*
* ESQueue worker function defaults `content` to null, even if the
* runTask returned undefined.
*
* This converts null to undefined so the value can be sent to h.response()
*/
// convert null to undefined so the value can be sent to h.response()
if (jobOutputContent === null) {
logger.warn('CSV Job Execution created empty content result');
}

View file

@ -74,8 +74,8 @@ describe('POST /api/reporting/generate', () => {
jobContentEncoding: 'base64',
jobContentExtension: 'pdf',
validLicenses: ['basic', 'gold'],
createJobFnFactory: () => () => ({ jobParamsTest: { test1: 'yes' } }),
runTaskFnFactory: () => () => ({ runParamsTest: { test2: 'yes' } }),
createJobFnFactory: () => async () => ({ createJobTest: { test1: 'yes' } } as any),
runTaskFnFactory: () => async () => ({ runParamsTest: { test2: 'yes' } } as any),
});
core.getExportTypesRegistry = () => mockExportTypesRegistry;
});
@ -163,9 +163,21 @@ describe('POST /api/reporting/generate', () => {
.then(({ body }) => {
expect(body).toMatchObject({
job: {
id: expect.any(String),
attempts: 0,
created_by: 'Tom Riddle',
id: 'foo',
index: 'foo-index',
jobtype: 'printable_pdf',
payload: {
createJobTest: {
test1: 'yes',
},
},
priority: 10,
status: 'pending',
timeout: 10000,
},
path: expect.any(String),
path: 'undefined/api/reporting/jobs/download/foo',
});
});
});

View file

@ -15,3 +15,10 @@ export function registerRoutes(reporting: ReportingCore, logger: Logger) {
registerJobInfoRoutes(reporting);
registerDiagnosticRoutes(reporting, logger);
}
export interface ReportingRequestPre {
management: {
jobTypes: string[];
};
user: string;
}

View file

@ -25,7 +25,6 @@ describe('GET /api/reporting/jobs/download', () => {
let core: ReportingCore;
const config = createMockConfig(createMockConfigSchema());
const getHits = (...sources: any) => {
return {
hits: {
@ -69,14 +68,14 @@ describe('GET /api/reporting/jobs/download', () => {
jobType: 'unencodedJobType',
jobContentExtension: 'csv',
validLicenses: ['basic', 'gold'],
} as ExportTypeDefinition<unknown, unknown, unknown, unknown>);
} as ExportTypeDefinition);
exportTypesRegistry.register({
id: 'base64Encoded',
jobType: 'base64EncodedJobType',
jobContentEncoding: 'base64',
jobContentExtension: 'pdf',
validLicenses: ['basic', 'gold'],
} as ExportTypeDefinition<unknown, unknown, unknown, unknown>);
} as ExportTypeDefinition);
core.getExportTypesRegistry = () => exportTypesRegistry;
});

View file

@ -128,7 +128,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
}
return res.ok({
body: jobOutput,
body: jobOutput || {},
headers: {
'content-type': 'application/json',
},

View file

@ -9,9 +9,9 @@ import contentDisposition from 'content-disposition';
import { get } from 'lodash';
import { CSV_JOB_TYPE } from '../../../common/constants';
import { ExportTypesRegistry, statuses } from '../../lib';
import { ExportTypeDefinition, JobSource, TaskRunResult } from '../../types';
type ExportTypeType = ExportTypeDefinition<unknown, unknown, unknown, unknown>;
import { ReportDocument } from '../../lib/store';
import { TaskRunResult } from '../../lib/tasks';
import { ExportTypeDefinition } from '../../types';
interface ErrorFromPayload {
message: string;
@ -27,10 +27,10 @@ interface Payload {
const DEFAULT_TITLE = 'report';
const getTitle = (exportType: ExportTypeType, title?: string): string =>
const getTitle = (exportType: ExportTypeDefinition, title?: string): string =>
`${title || DEFAULT_TITLE}.${exportType.jobContentExtension}`;
const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeType) => {
const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeDefinition) => {
const metaDataHeaders: Record<string, boolean> = {};
if (exportType.jobType === CSV_JOB_TYPE) {
@ -45,7 +45,10 @@ const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeType)
};
export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegistry) {
function encodeContent(content: string | null, exportType: ExportTypeType): Buffer | string {
function encodeContent(
content: string | null,
exportType: ExportTypeDefinition
): Buffer | string {
switch (exportType.jobContentEncoding) {
case 'base64':
return content ? Buffer.from(content, 'base64') : ''; // convert null to empty string
@ -55,7 +58,9 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist
}
function getCompleted(output: TaskRunResult, jobType: string, title: string): Payload {
const exportType = exportTypesRegistry.get((item: ExportTypeType) => item.jobType === jobType);
const exportType = exportTypesRegistry.get(
(item: ExportTypeDefinition) => item.jobType === jobType
);
const filename = getTitle(exportType, title);
const headers = getReportingHeaders(output, exportType);
@ -92,16 +97,18 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist
};
}
return function getDocumentPayload(doc: JobSource<unknown>): Payload {
return function getDocumentPayload(doc: ReportDocument): Payload {
const { status, jobtype: jobType, payload: { title } = { title: '' } } = doc._source;
const { output } = doc._source;
if (status === statuses.JOB_STATUS_COMPLETED || status === statuses.JOB_STATUS_WARNINGS) {
return getCompleted(output, jobType, title);
}
if (output) {
if (status === statuses.JOB_STATUS_COMPLETED || status === statuses.JOB_STATUS_WARNINGS) {
return getCompleted(output, jobType, title);
}
if (status === statuses.JOB_STATUS_FAILED) {
return getFailure(output);
if (status === statuses.JOB_STATUS_FAILED) {
return getFailure(output);
}
}
// send a 503 indicating that the report isn't completed yet

View file

@ -7,17 +7,13 @@
import { JobParamsPanelCsv } from '../../export_types/csv_from_savedobject/types';
import { CsvFromSavedObjectRequest } from '../generate_from_savedobject_immediate';
export function getJobParamsFromRequest(
request: CsvFromSavedObjectRequest,
opts: { isImmediate: boolean }
): JobParamsPanelCsv {
export function getJobParamsFromRequest(request: CsvFromSavedObjectRequest): JobParamsPanelCsv {
const { savedObjectType, savedObjectId } = request.params;
const { timerange, state } = request.body;
const post = timerange || state ? { timerange, state } : undefined;
return {
isImmediate: opts.isImmediate,
savedObjectType,
savedObjectId,
post,

View file

@ -8,7 +8,8 @@ import { i18n } from '@kbn/i18n';
import { errors as elasticsearchErrors } from 'elasticsearch';
import { get } from 'lodash';
import { ReportingCore } from '../../';
import { JobSource, ReportingUser } from '../../types';
import { ReportDocument } from '../../lib/store';
import { ReportingUser } from '../../types';
const esErrors = elasticsearchErrors as Record<string, any>;
const defaultSize = 10;
@ -130,7 +131,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore) {
});
},
get(user: ReportingUser, id: string, opts: GetOpts = {}): Promise<JobSource<unknown> | void> {
get(user: ReportingUser, id: string, opts: GetOpts = {}): Promise<ReportDocument | void> {
if (!id) return Promise.resolve();
const username = getUsername(user);

View file

@ -18,11 +18,11 @@ export type HandlerFunction = (
export type HandlerErrorFunction = (res: KibanaResponseFactory, err: Error) => any;
export interface QueuedJobPayload<JobParamsType> {
export interface QueuedJobPayload {
error?: boolean;
source: {
job: {
payload: BasePayload<JobParamsType>;
payload: BasePayload;
};
};
}

View file

@ -13,81 +13,11 @@ import { CancellationToken } from '../../../plugins/reporting/common';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server';
import { JobStatus } from '../common/types';
import { ReportingConfigType } from './config';
import { ReportingCore } from './core';
import { LevelLogger } from './lib';
import { LayoutInstance } from './lib/layouts';
/*
* Routing types
*/
export interface ReportingRequestPre {
management: {
jobTypes: string[];
};
user: string;
}
// generate a report with unparsed jobParams
export interface GenerateExportTypePayload {
jobParams: string;
}
export type ReportingRequestPayload = GenerateExportTypePayload | JobParamPostPayload;
export interface TimeRangeParams {
timezone: string;
min?: Date | string | number | null;
max?: Date | string | number | null;
}
// the "raw" data coming from the client, unencrypted
export interface JobParamPostPayload {
timerange?: TimeRangeParams;
}
// the pre-processed, encrypted data ready for storage
export interface BasePayload<JobParamsType> {
headers: string; // serialized encrypted headers
jobParams: JobParamsType;
title: string;
type: string;
spaceId?: string;
}
export interface JobSource<JobParamsType> {
_id: string;
_index: string;
_source: {
jobtype: string;
output: TaskRunResult;
payload: BasePayload<JobParamsType>;
status: JobStatus;
};
}
export interface TaskRunResult {
content_type: string | null;
content: string | null;
csv_contains_formulas?: boolean;
size: number;
max_size_reached?: boolean;
warnings?: string[];
}
interface ConditionalHeadersConditions {
protocol: string;
hostname: string;
port: number;
basePath: string;
}
export interface ConditionalHeaders {
headers: Record<string, string>;
conditions: ConditionalHeadersConditions;
}
import { LayoutParams } from './lib/layouts';
import { ReportTaskParams, TaskRunResult } from './lib/tasks';
/*
* Plugin Contract
@ -118,24 +48,29 @@ export type CaptureConfig = ReportingConfigType['capture'];
export type ScrollConfig = ReportingConfigType['csv']['scroll'];
export interface BaseParams {
browserTimezone: string;
layout?: LayoutInstance; // for screenshot type reports
browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface
layout?: LayoutParams;
objectType: string;
title: string;
}
export interface BaseParamsEncryptedFields extends BaseParams {
headers: string; // encrypted headers
// base params decorated with encrypted headers that come into runJob functions
export interface BasePayload extends BaseParams {
headers: string;
spaceId?: string;
}
export type CreateJobFn<JobParamsType extends BaseParams> = (
// default fn type for CreateJobFnFactory
export type CreateJobFn<JobParamsType = BaseParams, JobPayloadType = BasePayload> = (
jobParams: JobParamsType,
context: RequestHandlerContext,
request: KibanaRequest
) => Promise<JobParamsType & BaseParamsEncryptedFields>;
request: KibanaRequest<any, any, any, any>
) => Promise<JobPayloadType>;
export type RunTaskFn<TaskPayloadType> = (
// default fn type for RunTaskFnFactory
export type RunTaskFn<TaskPayloadType = BasePayload> = (
jobId: string,
job: TaskPayloadType,
payload: ReportTaskParams<TaskPayloadType>['payload'],
cancellationToken: CancellationToken
) => Promise<TaskRunResult>;
@ -149,12 +84,7 @@ export type RunTaskFnFactory<RunTaskFnType> = (
logger: LevelLogger
) => RunTaskFnType;
export interface ExportTypeDefinition<
JobParamsType,
CreateJobFnType,
JobPayloadType,
RunTaskFnType
> {
export interface ExportTypeDefinition<CreateJobFnType = CreateJobFn, RunTaskFnType = RunTaskFn> {
id: string;
name: string;
jobType: string;
@ -164,9 +94,3 @@ export interface ExportTypeDefinition<
runTaskFnFactory: RunTaskFnFactory<RunTaskFnType>;
validLicenses: string[];
}
export interface DiagnosticResponse {
help: string[];
success: boolean;
logs: string;
}