[Reporting] Convert more typescript in server/lib (#43496)

This commit is contained in:
Tim Sullivan 2019-08-19 14:07:46 -07:00 committed by GitHub
parent b33e1978a5
commit 91c8ad5595
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 231 additions and 116 deletions

View file

@ -5,25 +5,49 @@
*/
import { i18n } from '@kbn/i18n';
import { ElementHandle } from 'puppeteer';
import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver';
import { LevelLogger as Logger } from '../../../../server/lib/level_logger';
import { LayoutInstance } from '../../layouts/layout';
export const checkForToastMessage = async (
browser: HeadlessBrowser,
layout: LayoutInstance
): Promise<void> => {
await browser.waitForSelector(layout.selectors.toastHeader, { silent: true });
const toastHeaderText = await browser.evaluate({
fn: selector => {
const nodeList = document.querySelectorAll(selector);
return nodeList.item(0).innerText;
},
args: [layout.selectors.toastHeader],
});
throw new Error(
i18n.translate('xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage', {
defaultMessage: 'Encountered an unexpected message on the page: {toastHeaderText}',
values: { toastHeaderText },
})
);
layout: LayoutInstance,
logger: Logger
): Promise<ElementHandle<Element>> => {
return await browser
.waitForSelector(layout.selectors.toastHeader, { silent: true })
.then(async () => {
// Check for a toast message on the page. If there is one, capture the
// message and throw an error, to fail the screenshot.
const toastHeaderText: string = await browser.evaluate({
fn: selector => {
const nodeList = document.querySelectorAll(selector);
return nodeList.item(0).innerText;
},
args: [layout.selectors.toastHeader],
});
// Log an error to track the event in kibana server logs
logger.error(
i18n.translate(
'xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage',
{
defaultMessage: 'Encountered an unexpected message on the page: {toastHeaderText}',
values: { toastHeaderText },
}
)
);
// Throw an error to fail the screenshot job with a message
throw new Error(
i18n.translate(
'xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage',
{
defaultMessage: 'Encountered an unexpected message on the page: {toastHeaderText}',
values: { toastHeaderText },
}
)
);
});
};

View file

@ -71,7 +71,7 @@ export function screenshotsObservableFactory(server: KbnServer) {
const renderSuccess = browser.waitForSelector(
`${layout.selectors.renderComplete},[${layout.selectors.itemsCountAttribute}]`
);
const renderError = checkForToastMessage(browser, layout);
const renderError = checkForToastMessage(browser, layout, logger);
return Rx.race(Rx.from(renderSuccess), Rx.from(renderError));
},
browser => browser

View file

@ -4,27 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Request } from 'hapi';
import { oncePerServer } from '../../../server/lib/once_per_server';
import { cryptoFactory } from '../../../server/lib/crypto';
import { KbnServer, JobParams, ConditionalHeaders, CreateJobFactory } from '../../../types';
function createJobFn(server) {
function createJobFn(server: KbnServer) {
const crypto = cryptoFactory(server);
return async function createJob(jobParams, headers, request) {
return async function createJob(
jobParams: JobParams,
headers: ConditionalHeaders,
request: Request
) {
const serializedEncryptedHeaders = await crypto.encrypt(headers);
const savedObjectsClient = request.getSavedObjectsClient();
const indexPatternSavedObject = await savedObjectsClient.get(
'index-pattern',
jobParams.indexPatternId);
jobParams.indexPatternId!
);
return {
headers: serializedEncryptedHeaders,
indexPatternSavedObject: indexPatternSavedObject,
indexPatternSavedObject,
basePath: request.getBasePath(),
...jobParams
...jobParams,
};
};
}
export const createJobFactory = oncePerServer(createJobFn);
export const createJobFactory: CreateJobFactory = oncePerServer(createJobFn as CreateJobFactory);

View file

@ -8,7 +8,12 @@ import { Request } from 'hapi';
import { i18n } from '@kbn/i18n';
import { cryptoFactory, LevelLogger, oncePerServer } from '../../../server/lib';
import { JobDocOutputExecuted, JobDocPayload, KbnServer } from '../../../types';
import {
JobDocOutputExecuted,
JobDocPayload,
KbnServer,
ExecuteImmediateJobFactory,
} from '../../../types';
import {
CONTENT_TYPE_CSV,
CSV_FROM_SAVEDOBJECT_JOB_TYPE,
@ -107,4 +112,6 @@ function executeJobFactoryFn(server: KbnServer): ExecuteJobFn {
};
}
export const executeJobFactory = oncePerServer(executeJobFactoryFn);
export const executeJobFactory: ExecuteImmediateJobFactory = oncePerServer(
executeJobFactoryFn as ExecuteImmediateJobFactory
);

View file

@ -4,24 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Request } from 'hapi';
import { KbnServer, ConditionalHeaders } from '../../../../types';
import { cryptoFactory } from '../../../../server/lib/crypto';
import { oncePerServer } from '../../../../server/lib/once_per_server';
import { JobParamsPNG as JobParams, CreateJobFactoryPNG as CreateJobFactory } from '../../types';
function createJobFn(server) {
function createJobFn(server: KbnServer) {
const crypto = cryptoFactory(server);
return async function createJob({
objectType,
title,
relativeUrl,
browserTimezone,
layout
}, headers, request) {
return async function createJob(
{ objectType, title, relativeUrl, browserTimezone, layout }: JobParams,
headers: ConditionalHeaders,
request: Request
) {
const serializedEncryptedHeaders = await crypto.encrypt(headers);
return {
type: objectType,
title: title,
objectType,
title,
relativeUrl,
headers: serializedEncryptedHeaders,
browserTimezone,
@ -32,4 +33,4 @@ function createJobFn(server) {
};
}
export const createJobFactory = oncePerServer(createJobFn);
export const createJobFactory: CreateJobFactory = oncePerServer(createJobFn as CreateJobFactory);

View file

@ -7,7 +7,6 @@
import * as Rx from 'rxjs';
import { toArray, mergeMap } from 'rxjs/operators';
import { KbnServer, ConditionalHeaders } from '../../../../types';
// @ts-ignore
import { oncePerServer } from '../../../../server/lib/once_per_server';
import { screenshotsObservableFactory } from '../../../common/lib/screenshots';
import { PreserveLayout } from '../../../common/layouts/preserve_layout';

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Request } from 'hapi';
import { LayoutInstance } from '../common/layouts/layout';
import { ConditionalHeaders, KbnServer } from '../../types';
export interface JobParamsPNG {
objectType: string;
title: string;
relativeUrl: string;
browserTimezone: string;
layout: LayoutInstance;
}
export type ESQueueCreateJobFnPNG = (
jobParams: JobParamsPNG,
headers: ConditionalHeaders,
request: Request
) => Promise<JobParamsPNG>;
export type CreateJobFactoryPNG = (server: KbnServer) => ESQueueCreateJobFnPNG;

View file

@ -9,9 +9,8 @@ import { toArray, mergeMap } from 'rxjs/operators';
import moment from 'moment-timezone';
import { groupBy } from 'lodash';
import { KbnServer, ConditionalHeaders } from '../../../../types';
// @ts-ignore
// @ts-ignore untyped module
import { pdf } from './pdf';
// @ts-ignore
import { oncePerServer } from '../../../../server/lib/once_per_server';
import { screenshotsObservableFactory } from '../../../common/lib/screenshots';
import { createLayout } from '../../../common/layouts';

View file

@ -5,8 +5,7 @@
*/
import open from 'opn';
// @ts-ignore
import * as puppeteer from 'puppeteer-core';
import { Page } from 'puppeteer';
import { parse as parseUrl } from 'url';
import { ViewZoomWidthHeight } from '../../../../export_types/common/layouts/layout';
import {
@ -31,11 +30,11 @@ interface WaitForSelectorOpts {
const WAIT_FOR_DELAY_MS: number = 100;
export class HeadlessChromiumDriver {
private readonly page: puppeteer.Page;
private readonly page: Page;
private readonly logger: Logger;
private readonly inspect: boolean;
constructor(page: puppeteer.Page, { logger, inspect }: ChromiumDriverOptions) {
constructor(page: Page, { logger, inspect }: ChromiumDriverOptions) {
this.page = page;
// @ts-ignore https://github.com/elastic/kibana/issues/32140
this.logger = logger.clone(['headless-chromium-driver']);

View file

@ -8,6 +8,7 @@ import os from 'os';
import path from 'path';
// @ts-ignore
import puppeteer from 'puppeteer-core';
import { Browser, Page, LaunchOptions } from 'puppeteer';
import rimraf from 'rimraf';
import * as Rx from 'rxjs';
import { map, share, mergeMap, filter, partition, ignoreElements, tap } from 'rxjs/operators';
@ -71,7 +72,7 @@ export class HeadlessChromiumDriverFactory {
env: {
TZ: browserTimezone,
},
})
} as LaunchOptions)
.catch((error: Error) => {
logger.warning(
`The Reporting plugin encountered issues launching Chromium in a self-test. You may have trouble generating reports: [${error}]`
@ -105,8 +106,8 @@ export class HeadlessChromiumDriverFactory {
proxyConfig: this.browserConfig.proxy,
});
let browser: puppeteer.Browser;
let page: puppeteer.Page;
let browser: Browser;
let page: Page;
try {
browser = await puppeteer.launch({
pipe: true,
@ -117,7 +118,7 @@ export class HeadlessChromiumDriverFactory {
env: {
TZ: browserTimezone,
},
});
} as LaunchOptions);
page = await browser.newPage();

View file

@ -7,13 +7,11 @@
import { createHash } from 'crypto';
import { createReadStream } from 'fs';
// @ts-ignore
import { readableEnd } from './util';
export async function md5(path) {
export async function md5(path: string) {
const hash = createHash('md5');
await readableEnd(
createReadStream(path)
.on('data', chunk => hash.update(chunk))
);
await readableEnd(createReadStream(path).on('data', chunk => hash.update(chunk)));
return hash.digest('hex');
}

View file

@ -5,13 +5,16 @@
*/
import { PLUGIN_ID } from '../../common/constants';
import { KbnServer } from '../../types';
// @ts-ignore
import { Esqueue } from './esqueue';
import { createWorkerFactory } from './create_worker';
import { oncePerServer } from './once_per_server';
import { LevelLogger } from './level_logger';
// @ts-ignore
import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed
function createQueueFn(server) {
function createQueueFn(server: KbnServer): Esqueue {
const queueConfig = server.config().get('xpack.reporting.queue');
const index = server.config().get('xpack.reporting.index');
@ -23,7 +26,7 @@ function createQueueFn(server) {
logger: createTaggedLogger(server, [PLUGIN_ID, 'esqueue']),
};
const queue = new Esqueue(index, queueOptions);
const queue: Esqueue = new Esqueue(index, queueOptions);
if (queueConfig.pollEnabled) {
// create workers to poll the index for idle jobs waiting to be claimed and executed
@ -33,9 +36,9 @@ function createQueueFn(server) {
const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'create_queue']);
logger.info(
'xpack.reporting.queue.pollEnabled is set to false. This Kibana instance ' +
'will not poll for idle jobs to claim and execute. Make sure another ' +
'Kibana instance with polling enabled is running in this cluster so ' +
'reporting jobs can complete.',
'will not poll for idle jobs to claim and execute. Make sure another ' +
'Kibana instance with polling enabled is running in this cluster so ' +
'reporting jobs can complete.',
['info']
);
}

View file

@ -24,12 +24,14 @@ const executeJobFactoryStub = sinon.stub();
const getMockServer = (
exportTypes: any[] = [{ executeJobFactory: executeJobFactoryStub }]
): Partial<KbnServer> => ({
log: sinon.stub(),
expose: sinon.stub(),
config: () => ({ get: configGetStub }),
plugins: { reporting: { exportTypesRegistry: { getAll: () => exportTypes } } },
});
): KbnServer => {
return ({
log: sinon.stub(),
expose: sinon.stub(),
config: () => ({ get: configGetStub }),
plugins: { reporting: { exportTypesRegistry: { getAll: () => exportTypes } } },
} as unknown) as KbnServer;
};
describe('Create Worker', () => {
let queue: Esqueue;

View file

@ -16,7 +16,6 @@ import {
// @ts-ignore untyped dependency
import { events as esqueueEvents } from './esqueue';
import { LevelLogger } from './level_logger';
// @ts-ignore untyped dependency
import { oncePerServer } from './once_per_server';
function createWorkerFn(server: KbnServer) {

View file

@ -6,8 +6,9 @@
import nodeCrypto from '@elastic/node-crypto';
import { oncePerServer } from './once_per_server';
import { KbnServer } from '../../types';
function cryptoFn(server) {
function cryptoFn(server: KbnServer) {
const encryptionKey = server.config().get('xpack.reporting.encryptionKey');
return nodeCrypto({ encryptionKey });
}

View file

@ -5,17 +5,33 @@
*/
import { get } from 'lodash';
// @ts-ignore
import { events as esqueueEvents } from './esqueue';
import { oncePerServer } from './once_per_server';
import { KbnServer, Logger, JobParams, ConditionalHeaders } from '../../types';
function enqueueJobFn(server) {
interface ConfirmedJob {
id: string;
index: string;
_seq_no: number;
_primary_term: number;
}
function enqueueJobFn(server: KbnServer) {
const jobQueue = server.plugins.reporting.queue;
const config = server.config();
const queueConfig = config.get('xpack.reporting.queue');
const browserType = config.get('xpack.reporting.capture.browser.type');
const exportTypesRegistry = server.plugins.reporting.exportTypesRegistry;
return async function enqueueJob(parentLogger, exportTypeId, jobParams, user, headers, request) {
return async function enqueueJob(
parentLogger: Logger,
exportTypeId: string,
jobParams: JobParams,
user: string,
headers: ConditionalHeaders,
request: Request
) {
const logger = parentLogger.clone(['queue-job']);
const exportType = exportTypesRegistry.getById(exportTypeId);
const createJob = exportType.createJobFactory(server);
@ -30,7 +46,7 @@ function enqueueJobFn(server) {
return new Promise((resolve, reject) => {
const job = jobQueue.addJob(exportType.jobType, payload, options);
job.on(esqueueEvents.EVENT_JOB_CREATED, (createdJob) => {
job.on(esqueueEvents.EVENT_JOB_CREATED, (createdJob: ConfirmedJob) => {
if (createdJob.id === job.id) {
logger.info(`Successfully queued job: ${createdJob.id}`);
resolve(job);

View file

@ -4,13 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { resolve } from 'path';
import { resolve as pathResolve } from 'path';
import glob from 'glob';
import { KbnServer } from '../../types';
import { PLUGIN_ID } from '../../common/constants';
import { oncePerServer } from './once_per_server';
import { LevelLogger } from './level_logger';
// @ts-ignore untype module
import { ExportTypesRegistry } from '../../common/export_types_registry';
import { oncePerServer, LevelLogger } from './';
function scan(pattern) {
function scan(pattern: string) {
return new Promise((resolve, reject) => {
glob(pattern, {}, (err, files) => {
if (err) {
@ -22,16 +25,16 @@ function scan(pattern) {
});
}
const pattern = resolve(__dirname, '../../export_types/*/server/index.[jt]s');
async function exportTypesRegistryFn(server) {
const pattern = pathResolve(__dirname, '../../export_types/*/server/index.[jt]s');
async function exportTypesRegistryFn(server: KbnServer) {
const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'exportTypes']);
const exportTypesRegistry = new ExportTypesRegistry();
const files = await scan(pattern);
const files: string[] = (await scan(pattern)) as string[];
files.forEach(file => {
logger.debug(`Found exportType at ${file}`);
const { register } = require(file); // eslint-disable-line import/no-dynamic-require
const { register } = require(file); // eslint-disable-line @typescript-eslint/no-var-requires
register(exportTypesRegistry);
});
return exportTypesRegistry;

View file

@ -4,16 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore untyped module
export { cryptoFactory } from './crypto';
// @ts-ignore untyped module
export { oncePerServer } from './once_per_server';
// @ts-ignore untyped module
export { createQueueFactory } from './create_queue';
// @ts-ignore untyped module
export { exportTypesRegistryFactory } from './export_types_registry';
// @ts-ignore untyped module
export { checkLicenseFactory } from './check_license';
export { LevelLogger } from './level_logger';
export { createQueueFactory } from './create_queue';
export { cryptoFactory } from './crypto';
export { oncePerServer } from './once_per_server';
export { runValidations } from './validate';

View file

@ -4,7 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { memoize } from 'lodash';
import { memoize, MemoizedFunction } from 'lodash';
import { KbnServer } from '../../types';
type ServerFn = (server: KbnServer) => any;
type Memo = ((server: KbnServer) => any) & MemoizedFunction;
/**
* allow this function to be called multiple times, but
@ -17,8 +21,8 @@ import { memoize } from 'lodash';
* @param {Function} fn - the factory function
* @return {any}
*/
export function oncePerServer(fn) {
const memoized = memoize(function (server) {
export function oncePerServer(fn: ServerFn) {
const memoized: Memo = memoize(function(server: KbnServer) {
if (arguments.length !== 1) {
throw new TypeError('This function expects to be called with a single argument');
}
@ -27,13 +31,17 @@ export function oncePerServer(fn) {
throw new TypeError('This function expects to be passed the server');
}
// @ts-ignore
return fn.call(this, server);
});
// @ts-ignore
// Type 'WeakMap<object, any>' is not assignable to type 'MapCache
// use a weak map a the cache so that:
// 1. return values mapped to the actual server instance
// 2. return value lifecycle matches that of the server
memoized.cache = new WeakMap;
memoized.cache = new WeakMap();
return memoized;
}

View file

@ -6,7 +6,6 @@
import { ConfigObject, KbnServer, Logger } from '../../../types';
import { validateBrowser } from './validate_browser';
// @ts-ignore
import { validateConfig } from './validate_config';
import { validateMaxContentLength } from './validate_max_content_length';

View file

@ -3,8 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore no module definition
import * as puppeteer from 'puppeteer-core';
import { Browser } from 'puppeteer';
import { KbnServer, Logger } from '../../../types';
import { CHROMIUM } from '../../browsers/browser_types';
@ -21,7 +20,7 @@ export const validateBrowser = async (server: KbnServer, browserFactory: any, lo
},
logger
)
.then((browser: puppeteer.Browser | null) => {
.then((browser: Browser | null) => {
if (browser && browser.close) {
browser.close();
} else {

View file

@ -44,9 +44,7 @@ export function registerGenerateCsvFromSavedObjectImmediate(
const logger = parentLogger.clone(['savedobject-csv']);
const jobParams = getJobParamsFromRequest(request, { isImmediate: true });
const createJobFn = createJobFactory(server);
// By passing the request, we signal this as an "immediate" job.
// TODO: use a different executeFn for immediate, with a different call signature, to clean up messy types
const executeJobFn = executeJobFactory(server, request);
const executeJobFn = executeJobFactory(server);
const jobDocPayload: JobDocPayload = await createJobFn(jobParams, request.headers, request);
const {
content_type: jobOutputContentType,

View file

@ -8,7 +8,6 @@ import boom from 'boom';
import { Request, ResponseToolkit } from 'hapi';
import { API_BASE_URL } from '../../common/constants';
import { KbnServer, Logger } from '../../types';
// @ts-ignore
import { enqueueJobFactory } from '../lib/enqueue_job';
import { registerGenerate } from './generate';
import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject';

View file

@ -7,7 +7,6 @@
// @ts-ignore
import contentDisposition from 'content-disposition';
import * as _ from 'lodash';
// @ts-ignore
import { oncePerServer } from '../../lib/once_per_server';
import { CSV_JOB_TYPE } from '../../../common/constants';

View file

@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { memoize } from 'lodash';
import { KbnServer } from '../types';
export const createMockServer = ({ settings = {} }: any) => {
export const createMockServer = ({ settings = {} }: any): KbnServer => {
const mockServer = {
expose: () => {
' ';
@ -40,5 +41,5 @@ export const createMockServer = ({ settings = {} }: any) => {
return key in settings ? settings[key] : defaultSettings[key];
});
return mockServer;
return (mockServer as unknown) as KbnServer;
};

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Request } from 'hapi';
interface UiSettings {
get: (value: string) => string;
}
@ -32,20 +34,6 @@ export interface KbnServer {
}) => UiSettings;
}
export interface ExportTypeDefinition {
id: string;
name: string;
jobType: string;
jobContentExtension: string;
createJobFactory: () => any;
executeJobFactory: () => any;
validLicenses: string[];
}
export interface ExportTypesRegistry {
register: (exportTypeDefinition: ExportTypeDefinition) => void;
}
export interface ConfigObject {
get: (path?: string) => any;
}
@ -119,6 +107,8 @@ export interface JobParams {
post?: JobParamPostPayload;
panel?: any; // has to be resolved by the request handler
visType?: string; // has to be resolved by the request handler
indexPatternId?: string; // for export_types/csv
}
export interface JobDocPayload {
@ -134,6 +124,19 @@ export interface JobDocPayload {
objects?: string | null; // string if completed job; null if incomplete job;
}
export type JobDocPayloadScreenshot = JobDocPayload & {
browserTimezone: string;
layout: any;
};
export type JobDocPayloadDiscoverCsv = JobDocPayload & {
searchRequest: any;
fields: any;
indexPatternSavedObject: any;
metaFields: any;
conflictedTypesFields: any;
};
export interface JobDocOutput {
content: string; // encoded content
contentType: string;
@ -175,13 +178,17 @@ export interface ESQueueWorker {
on: (event: string, handler: any) => void;
}
export type ESQueueWorkerExecuteFn = (job: JobDoc, cancellationToken: any) => void;
export type ESQueueCreateJobFn = (
jobParams: JobParams,
headers: ConditionalHeaders,
request: Request
) => Promise<JobParams>;
export interface ExportType {
jobType: string;
createJobFactory: any;
executeJobFactory: (server: KbnServer) => ESQueueWorkerExecuteFn;
}
export type ESQueueWorkerExecuteFn = (job: JobDoc, cancellationToken: any) => void;
export type ImmediateExecuteFn = (
jobDocPayload: JobDocPayload,
request: Request
) => Promise<JobDocOutputExecuted>;
export interface ESQueueWorkerOptions {
kibanaName: string;
@ -198,4 +205,28 @@ export interface ESQueueInstance {
) => ESQueueWorker;
}
export type CreateJobFactory = (server: KbnServer) => ESQueueCreateJobFn;
export type ExecuteJobFactory = (server: KbnServer) => ESQueueWorkerExecuteFn;
export type ExecuteImmediateJobFactory = (server: KbnServer) => ImmediateExecuteFn;
export interface ExportTypeDefinition {
id: string;
name: string;
jobType: string;
jobContentExtension: string;
createJobFactory: CreateJobFactory;
executeJobFactory: ExecuteJobFactory | ExecuteImmediateJobFactory;
validLicenses: string[];
}
export interface ExportType {
jobType: string;
createJobFactory: any;
executeJobFactory: (server: KbnServer) => ESQueueWorkerExecuteFn;
}
export interface ExportTypesRegistry {
register: (exportTypeDefinition: ExportTypeDefinition) => void;
}
export { LevelLogger as Logger } from './server/lib/level_logger';