From 8d54fa8363039a0ded5de96bb78c44b6b6bb2403 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Fri, 31 Jan 2020 13:47:41 -0700 Subject: [PATCH] [Canvas] Migrate Shareable Routes to NP (#56053) * Migrated shareables routes to NP * Added tests --- x-pack/legacy/plugins/canvas/server/plugin.ts | 3 - .../plugins/canvas/server/routes/index.ts | 12 --- .../canvas/server/routes/shareables.ts | 72 ----------------- x-pack/plugins/canvas/server/routes/index.ts | 6 +- .../server/routes/shareables/download.test.ts | 55 +++++++++++++ .../server/routes/shareables/download.ts | 33 ++++++++ .../canvas/server/routes/shareables/index.ts | 14 ++++ .../shareables/mock_shareable_workpad.json | 0 .../shareables/rendered_workpad_schema.ts | 77 ++++++++++++++++++ .../server/routes/shareables/zip.test.ts | 78 +++++++++++++++++++ .../canvas/server/routes/shareables/zip.ts | 37 +++++++++ 11 files changed, 298 insertions(+), 89 deletions(-) delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/index.ts delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/shareables.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/download.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/download.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json create mode 100644 x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/zip.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/zip.ts diff --git a/x-pack/legacy/plugins/canvas/server/plugin.ts b/x-pack/legacy/plugins/canvas/server/plugin.ts index ac3edbabce930e..1f17e85bfd294f 100644 --- a/x-pack/legacy/plugins/canvas/server/plugin.ts +++ b/x-pack/legacy/plugins/canvas/server/plugin.ts @@ -5,14 +5,11 @@ */ import { CoreSetup, PluginsSetup } from './shim'; -import { routes } from './routes'; import { functions } from '../canvas_plugin_src/functions/server'; import { loadSampleData } from './sample_data'; export class Plugin { public setup(core: CoreSetup, plugins: PluginsSetup) { - routes(core); - plugins.interpreter.register({ serverFunctions: functions }); core.injectUiAppVars('canvas', async () => { diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts deleted file mode 100644 index 6898a3c459e3d4..00000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * 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 { shareableWorkpads } from './shareables'; -import { CoreSetup } from '../shim'; - -export function routes(setup: CoreSetup): void { - shareableWorkpads(setup.http.route); -} diff --git a/x-pack/legacy/plugins/canvas/server/routes/shareables.ts b/x-pack/legacy/plugins/canvas/server/routes/shareables.ts deleted file mode 100644 index e8186ceceb47f2..00000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/shareables.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 archiver from 'archiver'; - -import { - API_ROUTE_SHAREABLE_RUNTIME, - API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, - API_ROUTE_SHAREABLE_ZIP, -} from '../../common/lib/constants'; - -import { - SHAREABLE_RUNTIME_FILE, - SHAREABLE_RUNTIME_NAME, - SHAREABLE_RUNTIME_SRC, -} from '../../shareable_runtime/constants'; - -import { CoreSetup } from '../shim'; - -export function shareableWorkpads(route: CoreSetup['http']['route']) { - // get runtime - route({ - method: 'GET', - path: API_ROUTE_SHAREABLE_RUNTIME, - - handler: { - file: { - path: SHAREABLE_RUNTIME_FILE, - // The option setting is not for typical use. We're using it here to avoid - // problems in Cloud environments. See elastic/kibana#47405. - confine: false, - }, - }, - }); - - // download runtime - route({ - method: 'GET', - path: API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, - - handler(_request, handler) { - // The option setting is not for typical use. We're using it here to avoid - // problems in Cloud environments. See elastic/kibana#47405. - // @ts-ignore No type for inert Hapi handler - const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false }); - file.type('application/octet-stream'); - return file; - }, - }); - - route({ - method: 'POST', - path: API_ROUTE_SHAREABLE_ZIP, - handler(request, handler) { - const workpad = request.payload; - - const archive = archiver('zip'); - archive.append(JSON.stringify(workpad), { name: 'workpad.json' }); - archive.file(`${SHAREABLE_RUNTIME_SRC}/template.html`, { name: 'index.html' }); - archive.file(SHAREABLE_RUNTIME_FILE, { name: `${SHAREABLE_RUNTIME_NAME}.js` }); - - const response = handler.response(archive); - response.header('content-type', 'application/zip'); - archive.finalize(); - - return response; - }, - }); -} diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts index e9afab5680332d..fce278e94bf32f 100644 --- a/x-pack/plugins/canvas/server/routes/index.ts +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -5,9 +5,10 @@ */ import { IRouter, Logger } from 'src/core/server'; -import { initWorkpadRoutes } from './workpad'; import { initCustomElementsRoutes } from './custom_elements'; import { initESFieldsRoutes } from './es_fields'; +import { initShareablesRoutes } from './shareables'; +import { initWorkpadRoutes } from './workpad'; export interface RouteInitializerDeps { router: IRouter; @@ -15,7 +16,8 @@ export interface RouteInitializerDeps { } export function initRoutes(deps: RouteInitializerDeps) { - initWorkpadRoutes(deps); initCustomElementsRoutes(deps); initESFieldsRoutes(deps); + initShareablesRoutes(deps); + initWorkpadRoutes(deps); } diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.test.ts b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts new file mode 100644 index 00000000000000..be4765217d7aa8 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('fs'); + +import fs from 'fs'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { httpServiceMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; +import { initializeDownloadShareableWorkpadRoute } from './download'; + +const mockRouteContext = {} as RequestHandlerContext; +const path = `api/canvas/workpad/find`; +const mockRuntime = 'Canvas shareable runtime'; + +describe('Download Canvas shareables runtime', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDownloadShareableWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it(`returns 200 with canvas shareables runtime`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path, + }); + + const readFileSyncMock = fs.readFileSync as jest.Mock; + readFileSyncMock.mockReturnValueOnce(mockRuntime); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(`"Canvas shareable runtime"`); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.ts b/x-pack/plugins/canvas/server/routes/shareables/download.ts new file mode 100644 index 00000000000000..08bec1e4881aeb --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/download.ts @@ -0,0 +1,33 @@ +/* + * 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 { readFileSync } from 'fs'; +import { SHAREABLE_RUNTIME_FILE } from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; +import { RouteInitializerDeps } from '../'; +import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeDownloadShareableWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, + validate: false, + }, + async (_context, _request, response) => { + // TODO: check if this is still an issue on cloud after migrating to NP + // + // The option setting is not for typical use. We're using it here to avoid + // problems in Cloud environments. See elastic/kibana#47405. + // @ts-ignore No type for inert Hapi handler + // const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false }); + const file = readFileSync(SHAREABLE_RUNTIME_FILE); + return response.ok({ + headers: { 'content-type': 'application/octet-stream' }, + body: file, + }); + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/shareables/index.ts b/x-pack/plugins/canvas/server/routes/shareables/index.ts new file mode 100644 index 00000000000000..0aabd8b955b211 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { initializeZipShareableWorkpadRoute } from './zip'; +import { initializeDownloadShareableWorkpadRoute } from './download'; + +export function initShareablesRoutes(deps: RouteInitializerDeps) { + initializeDownloadShareableWorkpadRoute(deps); + initializeZipShareableWorkpadRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json b/x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts b/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts new file mode 100644 index 00000000000000..792200354724e4 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const PositionSchema = schema.object({ + angle: schema.number(), + height: schema.number(), + left: schema.number(), + parent: schema.nullable(schema.string()), + top: schema.number(), + width: schema.number(), +}); + +export const ContainerStyleSchema = schema.object({ + type: schema.maybe(schema.string()), + border: schema.maybe(schema.string()), + borderRadius: schema.maybe(schema.string()), + padding: schema.maybe(schema.string()), + backgroundColor: schema.maybe(schema.string()), + backgroundImage: schema.maybe(schema.string()), + backgroundSize: schema.maybe(schema.string()), + backgroundRepeat: schema.maybe(schema.string()), + opacity: schema.maybe(schema.number()), + overflow: schema.maybe(schema.string()), +}); + +export const RenderableSchema = schema.object({ + error: schema.nullable(schema.string()), + state: schema.string(), + value: schema.object({ + as: schema.string(), + containerStyle: ContainerStyleSchema, + css: schema.maybe(schema.string()), + type: schema.string(), + value: schema.any(), + }), +}); + +export const RenderedWorkpadElementSchema = schema.object({ + expressionRenderable: RenderableSchema, + id: schema.string(), + position: PositionSchema, +}); + +export const RenderedWorkpadPageSchema = schema.object({ + id: schema.string(), + elements: schema.arrayOf(RenderedWorkpadElementSchema), + groups: schema.maybe(schema.arrayOf(schema.arrayOf(RenderedWorkpadElementSchema))), + style: schema.recordOf(schema.string(), schema.string()), + transition: schema.maybe( + schema.oneOf([ + schema.object({}), + schema.object({ + name: schema.string(), + }), + ]) + ), +}); + +export const RenderedWorkpadSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + assets: schema.maybe(schema.recordOf(schema.string(), RenderedWorkpadPageSchema)), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + height: schema.number(), + id: schema.string(), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(RenderedWorkpadPageSchema), + width: schema.number(), +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts new file mode 100644 index 00000000000000..edb59694a74005 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts @@ -0,0 +1,78 @@ +/* + * 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. + */ + +jest.mock('archiver'); + +const archiver = require('archiver') as jest.Mock; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { httpServiceMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; +import { initializeZipShareableWorkpadRoute } from './zip'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../legacy/plugins/canvas/common/lib'; +import { + SHAREABLE_RUNTIME_FILE, + SHAREABLE_RUNTIME_SRC, + SHAREABLE_RUNTIME_NAME, +} from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; + +const mockRouteContext = {} as RequestHandlerContext; +const mockWorkpad = {}; +const routePath = API_ROUTE_SHAREABLE_ZIP; + +describe('Zips Canvas shareables runtime together with workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeZipShareableWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it(`returns 200 with zip file with runtime and workpad`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: routePath, + body: mockWorkpad, + }); + + const mockArchive = { + append: jest.fn(), + file: jest.fn(), + finalize: jest.fn(), + }; + + archiver.mockReturnValueOnce(mockArchive); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toBe(mockArchive); + expect(mockArchive.append).toHaveBeenCalledWith(JSON.stringify(mockWorkpad), { + name: 'workpad.json', + }); + expect(mockArchive.file).toHaveBeenCalledTimes(2); + expect(mockArchive.file).nthCalledWith(1, `${SHAREABLE_RUNTIME_SRC}/template.html`, { + name: 'index.html', + }); + expect(mockArchive.file).nthCalledWith(2, SHAREABLE_RUNTIME_FILE, { + name: `${SHAREABLE_RUNTIME_NAME}.js`, + }); + expect(mockArchive.finalize).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/zip.ts b/x-pack/plugins/canvas/server/routes/shareables/zip.ts new file mode 100644 index 00000000000000..e25b96cce96ff3 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/zip.ts @@ -0,0 +1,37 @@ +/* + * 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 archiver from 'archiver'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../legacy/plugins/canvas/common/lib'; +import { + SHAREABLE_RUNTIME_FILE, + SHAREABLE_RUNTIME_NAME, + SHAREABLE_RUNTIME_SRC, +} from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; +import { RenderedWorkpadSchema } from './rendered_workpad_schema'; +import { RouteInitializerDeps } from '..'; + +export function initializeZipShareableWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: API_ROUTE_SHAREABLE_ZIP, + validate: { body: RenderedWorkpadSchema }, + }, + async (_context, request, response) => { + const workpad = request.body; + const archive = archiver('zip'); + archive.append(JSON.stringify(workpad), { name: 'workpad.json' }); + archive.file(`${SHAREABLE_RUNTIME_SRC}/template.html`, { name: 'index.html' }); + archive.file(SHAREABLE_RUNTIME_FILE, { name: `${SHAREABLE_RUNTIME_NAME}.js` }); + + const result = { headers: { 'content-type': 'application/zip' }, body: archive }; + archive.finalize(); + + return response.ok(result); + } + ); +}