[Canvas] Move workpad api routes to New Platform (#51116)

* Move workpad api routes to New Platform

* Cleanup

* Clean up/Pr Feedback

* Adding missing dependency to tests

* Fix typecheck

* Loosen workpad schema restrictions
This commit is contained in:
Corey Robertson 2019-11-26 14:22:02 -05:00 committed by GitHub
parent 3b6e51b2d8
commit f5296293c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1201 additions and 718 deletions

View file

@ -192,3 +192,16 @@ export const elements: CanvasElement[] = [
{ ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' },
{ ...BaseElement, expression: 'image | render' },
];
export const workpadWithGroupAsElement: CanvasWorkpad = {
...BaseWorkpad,
pages: [
{
...BasePage,
elements: [
{ ...BaseElement, expression: 'image | render' },
{ ...BaseElement, id: 'group-1234' },
],
},
],
};

View file

@ -29,6 +29,7 @@ export function get(workpadId) {
});
}
// TODO: I think this function is never used. Look into and remove the corresponding route as well
export function update(id, workpad) {
return fetch.put(`${apiPath}/${id}`, workpad);
}

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { workpad } from './workpad';
import { esFields } from './es_fields';
import { customElements } from './custom_elements';
import { shareableWorkpads } from './shareables';
@ -13,6 +12,5 @@ import { CoreSetup } from '../shim';
export function routes(setup: CoreSetup): void {
customElements(setup.http.route, setup.elasticsearch);
esFields(setup.http.route, setup.elasticsearch);
workpad(setup.http.route, setup.elasticsearch);
shareableWorkpads(setup.http.route);
}

View file

@ -1,462 +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 Hapi from 'hapi';
import {
CANVAS_TYPE,
API_ROUTE_WORKPAD,
API_ROUTE_WORKPAD_ASSETS,
API_ROUTE_WORKPAD_STRUCTURES,
} from '../../common/lib/constants';
import { workpad } from './workpad';
const routePrefix = API_ROUTE_WORKPAD;
const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS;
const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES;
jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc'));
describe(`${CANVAS_TYPE} API`, () => {
const savedObjectsClient = {
get: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
};
afterEach(() => {
savedObjectsClient.get.mockReset();
savedObjectsClient.create.mockReset();
savedObjectsClient.delete.mockReset();
savedObjectsClient.find.mockReset();
});
// Mock toISOString function of all Date types
global.Date = class Date extends global.Date {
toISOString() {
return '2019-02-12T21:01:22.479Z';
}
};
// Setup mock server
const mockServer = new Hapi.Server({ debug: false, port: 0 });
const mockEs = {
getCluster: () => ({
errors: {
// formatResponse will fail without objects here
'400': Error,
'401': Error,
'403': Error,
'404': Error,
},
}),
};
mockServer.ext('onRequest', (req, h) => {
req.getSavedObjectsClient = () => savedObjectsClient;
return h.continue;
});
workpad(mockServer.route.bind(mockServer), mockEs);
describe(`GET ${routePrefix}/{id}`, () => {
test('returns successful response', async () => {
const request = {
method: 'GET',
url: `${routePrefix}/123`,
};
savedObjectsClient.get.mockResolvedValueOnce({ id: '123', attributes: { foo: true } });
const { payload, statusCode } = await mockServer.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toMatchInlineSnapshot(`
Object {
"foo": true,
"id": "123",
}
`);
expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
"123",
],
]
`);
});
});
describe(`POST ${routePrefix}`, () => {
test('returns successful response without id in payload', async () => {
const request = {
method: 'POST',
url: routePrefix,
payload: {
foo: true,
},
};
savedObjectsClient.create.mockResolvedValueOnce({});
const { payload, statusCode } = await mockServer.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toMatchInlineSnapshot(`
Object {
"ok": true,
}
`);
expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
Object {
"@created": "2019-02-12T21:01:22.479Z",
"@timestamp": "2019-02-12T21:01:22.479Z",
"foo": true,
},
Object {
"id": "workpad-123abc",
},
],
]
`);
});
test('returns succesful response with id in payload', async () => {
const request = {
method: 'POST',
url: routePrefix,
payload: {
id: '123',
foo: true,
},
};
savedObjectsClient.create.mockResolvedValueOnce({});
const { payload, statusCode } = await mockServer.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toMatchInlineSnapshot(`
Object {
"ok": true,
}
`);
expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
Object {
"@created": "2019-02-12T21:01:22.479Z",
"@timestamp": "2019-02-12T21:01:22.479Z",
"foo": true,
},
Object {
"id": "123",
},
],
]
`);
});
});
describe(`PUT ${routePrefix}/{id}`, () => {
test('formats successful response', async () => {
const request = {
method: 'PUT',
url: `${routePrefix}/123`,
payload: {
id: '234',
foo: true,
},
};
savedObjectsClient.get.mockResolvedValueOnce({
attributes: {
'@created': new Date().toISOString(),
},
});
savedObjectsClient.create.mockResolvedValueOnce({});
const { payload, statusCode } = await mockServer.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toMatchInlineSnapshot(`
Object {
"ok": true,
}
`);
expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
"123",
],
]
`);
expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
Object {
"@created": "2019-02-12T21:01:22.479Z",
"@timestamp": "2019-02-12T21:01:22.479Z",
"foo": true,
},
Object {
"id": "123",
"overwrite": true,
},
],
]
`);
});
});
describe(`DELETE ${routePrefix}/{id}`, () => {
test('formats successful response', async () => {
const request = {
method: 'DELETE',
url: `${routePrefix}/123`,
};
savedObjectsClient.delete.mockResolvedValueOnce({});
const { payload, statusCode } = await mockServer.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toMatchInlineSnapshot(`
Object {
"ok": true,
}
`);
expect(savedObjectsClient.delete.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
"123",
],
]
`);
});
});
it(`GET ${routePrefix}/find`, async () => {
const request = {
method: 'GET',
url: `${routePrefix}/find?name=abc&page=2&perPage=10`,
};
savedObjectsClient.find.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
attributes: {
foo: true,
},
},
],
});
const { payload, statusCode } = await mockServer.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toMatchInlineSnapshot(`
Object {
"workpads": Array [
Object {
"foo": true,
"id": "1",
},
],
}
`);
expect(savedObjectsClient.find.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"fields": Array [
"id",
"name",
"@created",
"@timestamp",
],
"page": "2",
"perPage": "10",
"search": "abc* | abc",
"searchFields": Array [
"name",
],
"sortField": "@timestamp",
"sortOrder": "desc",
"type": "canvas-workpad",
},
],
]
`);
});
describe(`PUT ${routePrefixAssets}/{id}`, () => {
test('only updates assets', async () => {
const request = {
method: 'PUT',
url: `${routePrefixAssets}/123`,
payload: {
'asset-123': {
id: 'asset-123',
'@created': '2019-02-14T00:00:00.000Z',
type: 'dataurl',
value: 'mockbase64data',
},
'asset-456': {
id: 'asset-456',
'@created': '2019-02-15T00:00:00.000Z',
type: 'dataurl',
value: 'mockbase64data',
},
},
};
// provide some existing workpad data to check that it's preserved
savedObjectsClient.get.mockResolvedValueOnce({
attributes: {
'@created': new Date().toISOString(),
name: 'fake workpad',
},
});
savedObjectsClient.create.mockResolvedValueOnce({});
const { payload, statusCode } = await mockServer.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toMatchInlineSnapshot(`
Object {
"ok": true,
}
`);
expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
"123",
],
]
`);
expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
Object {
"@created": "2019-02-12T21:01:22.479Z",
"@timestamp": "2019-02-12T21:01:22.479Z",
"assets": Object {
"asset-123": Object {
"@created": "2019-02-14T00:00:00.000Z",
"id": "asset-123",
"type": "dataurl",
"value": "mockbase64data",
},
"asset-456": Object {
"@created": "2019-02-15T00:00:00.000Z",
"id": "asset-456",
"type": "dataurl",
"value": "mockbase64data",
},
},
"name": "fake workpad",
},
Object {
"id": "123",
"overwrite": true,
},
],
]
`);
});
});
describe(`PUT ${routePrefixStructures}/{id}`, () => {
test('only updates workpad', async () => {
const request = {
method: 'PUT',
url: `${routePrefixStructures}/123`,
payload: {
name: 'renamed workpad',
css: '.canvasPage { color: LavenderBlush; }',
},
};
// provide some existing asset data and a name to replace
savedObjectsClient.get.mockResolvedValueOnce({
attributes: {
'@created': new Date().toISOString(),
name: 'fake workpad',
assets: {
'asset-123': {
id: 'asset-123',
'@created': '2019-02-14T00:00:00.000Z',
type: 'dataurl',
value: 'mockbase64data',
},
},
},
});
savedObjectsClient.create.mockResolvedValueOnce({});
const { payload, statusCode } = await mockServer.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toMatchInlineSnapshot(`
Object {
"ok": true,
}
`);
expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
"123",
],
]
`);
expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
Object {
"@created": "2019-02-12T21:01:22.479Z",
"@timestamp": "2019-02-12T21:01:22.479Z",
"assets": Object {
"asset-123": Object {
"@created": "2019-02-14T00:00:00.000Z",
"id": "asset-123",
"type": "dataurl",
"value": "mockbase64data",
},
},
"css": ".canvasPage { color: LavenderBlush; }",
"name": "renamed workpad",
},
Object {
"id": "123",
"overwrite": true,
},
],
]
`);
});
});
});

View file

@ -1,254 +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 boom from 'boom';
import { omit } from 'lodash';
import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/server';
import {
CANVAS_TYPE,
API_ROUTE_WORKPAD,
API_ROUTE_WORKPAD_ASSETS,
API_ROUTE_WORKPAD_STRUCTURES,
} from '../../common/lib/constants';
import { getId } from '../../public/lib/get_id';
import { CoreSetup } from '../shim';
// @ts-ignore Untyped Local
import { formatResponse as formatRes } from '../lib/format_response';
import { CanvasWorkpad } from '../../types';
type WorkpadAttributes = Pick<CanvasWorkpad, Exclude<keyof CanvasWorkpad, 'id'>> & {
'@timestamp': string;
'@created': string;
};
interface WorkpadRequestFacade {
getSavedObjectsClient: () => SavedObjectsClientContract;
}
type WorkpadRequest = WorkpadRequestFacade & {
params: {
id: string;
};
payload: CanvasWorkpad;
};
type FindWorkpadRequest = WorkpadRequestFacade & {
query: {
name: string;
page: number;
perPage: number;
};
};
type AssetsRequest = WorkpadRequestFacade & {
params: {
id: string;
};
payload: CanvasWorkpad['assets'];
};
export function workpad(
route: CoreSetup['http']['route'],
elasticsearch: CoreSetup['elasticsearch']
) {
// @ts-ignore EsErrors is not on the Cluster type
const { errors: esErrors } = elasticsearch.getCluster('data');
const routePrefix = API_ROUTE_WORKPAD;
const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS;
const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES;
const formatResponse = formatRes(esErrors);
function createWorkpad(req: WorkpadRequest) {
const savedObjectsClient = req.getSavedObjectsClient();
if (!req.payload) {
return Promise.reject(boom.badRequest('A workpad payload is required'));
}
const now = new Date().toISOString();
const { id, ...payload } = req.payload;
return savedObjectsClient.create<WorkpadAttributes>(
CANVAS_TYPE,
{
...payload,
'@timestamp': now,
'@created': now,
},
{ id: id || getId('workpad') }
);
}
function updateWorkpad(
req: WorkpadRequest | AssetsRequest,
newPayload?: CanvasWorkpad | { assets: CanvasWorkpad['assets'] }
) {
const savedObjectsClient = req.getSavedObjectsClient();
const { id } = req.params;
const payload = newPayload ? newPayload : req.payload;
const now = new Date().toISOString();
return savedObjectsClient.get<WorkpadAttributes>(CANVAS_TYPE, id).then(workpadObject => {
// TODO: Using create with force over-write because of version conflict issues with update
return savedObjectsClient.create(
CANVAS_TYPE,
{
...(workpadObject.attributes as SavedObjectAttributes),
...omit(payload, 'id'), // never write the id property
'@timestamp': now, // always update the modified time
'@created': workpadObject.attributes['@created'], // ensure created is not modified
},
{ overwrite: true, id }
);
});
}
function deleteWorkpad(req: WorkpadRequest) {
const savedObjectsClient = req.getSavedObjectsClient();
const { id } = req.params;
return savedObjectsClient.delete(CANVAS_TYPE, id);
}
function findWorkpad(req: FindWorkpadRequest) {
const savedObjectsClient = req.getSavedObjectsClient();
const { name, page, perPage } = req.query;
return savedObjectsClient.find({
type: CANVAS_TYPE,
sortField: '@timestamp',
sortOrder: 'desc',
search: name ? `${name}* | ${name}` : '*',
searchFields: ['name'],
fields: ['id', 'name', '@created', '@timestamp'],
page,
perPage,
});
}
// get workpad
route({
method: 'GET',
path: `${routePrefix}/{id}`,
handler(req: WorkpadRequest) {
const savedObjectsClient = req.getSavedObjectsClient();
const { id } = req.params;
return savedObjectsClient
.get<WorkpadAttributes>(CANVAS_TYPE, id)
.then(obj => {
if (
// not sure if we need to be this defensive
obj.type === 'canvas-workpad' &&
obj.attributes &&
obj.attributes.pages &&
obj.attributes.pages.length
) {
obj.attributes.pages.forEach(page => {
const elements = (page.elements || []).filter(
({ id: pageId }) => !pageId.startsWith('group')
);
const groups = (page.groups || []).concat(
(page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group'))
);
page.elements = elements;
page.groups = groups;
});
}
return obj;
})
.then(obj => ({ id: obj.id, ...obj.attributes }))
.then(formatResponse)
.catch(formatResponse);
},
});
// create workpad
route({
method: 'POST',
path: routePrefix,
// @ts-ignore config option missing on route method type
config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit
handler(request: WorkpadRequest) {
return createWorkpad(request)
.then(() => ({ ok: true }))
.catch(formatResponse);
},
});
// update workpad
route({
method: 'PUT',
path: `${routePrefix}/{id}`,
// @ts-ignore config option missing on route method type
config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit
handler(request: WorkpadRequest) {
return updateWorkpad(request)
.then(() => ({ ok: true }))
.catch(formatResponse);
},
});
// update workpad assets
route({
method: 'PUT',
path: `${routePrefixAssets}/{id}`,
// @ts-ignore config option missing on route method type
config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit
handler(request: AssetsRequest) {
const payload = { assets: request.payload };
return updateWorkpad(request, payload)
.then(() => ({ ok: true }))
.catch(formatResponse);
},
});
// update workpad structures
route({
method: 'PUT',
path: `${routePrefixStructures}/{id}`,
// @ts-ignore config option missing on route method type
config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit
handler(request: WorkpadRequest) {
return updateWorkpad(request)
.then(() => ({ ok: true }))
.catch(formatResponse);
},
});
// delete workpad
route({
method: 'DELETE',
path: `${routePrefix}/{id}`,
handler(request: WorkpadRequest) {
return deleteWorkpad(request)
.then(() => ({ ok: true }))
.catch(formatResponse);
},
});
// find workpads
route({
method: 'GET',
path: `${routePrefix}/find`,
handler(request: FindWorkpadRequest) {
return findWorkpad(request)
.then(formatResponse)
.then(resp => {
return {
total: resp.total,
workpads: resp.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })),
};
})
.catch(() => {
return {
total: 0,
workpads: [],
};
});
},
});
}

View file

@ -0,0 +1,10 @@
{
"id": "canvas",
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "canvas"],
"server": true,
"ui": false,
"requiredPlugins": []
}

View file

@ -0,0 +1,11 @@
/*
* 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 { PluginInitializerContext } from 'src/core/server';
import { CanvasPlugin } from './plugin';
export const plugin = (initializerContext: PluginInitializerContext) =>
new CanvasPlugin(initializerContext);

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 { CoreSetup, PluginInitializerContext, Plugin, Logger } from 'src/core/server';
import { initRoutes } from './routes';
export class CanvasPlugin implements Plugin {
private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
public setup(coreSetup: CoreSetup): void {
const canvasRouter = coreSetup.http.createRouter();
initRoutes({ router: canvasRouter, logger: this.logger });
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,30 @@
/*
* 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 { ObjectType } from '@kbn/config-schema';
import { RequestHandler } from 'src/core/server';
export const catchErrorHandler: <
P extends ObjectType<any>,
Q extends ObjectType<any>,
B extends ObjectType<any>
>(
fn: RequestHandler<P, Q, B>
) => RequestHandler<P, Q, B> = fn => {
return async (context, request, response) => {
try {
return await fn(context, request, response);
} catch (error) {
if (error.isBoom) {
return response.customError({
body: error.output.payload,
statusCode: error.output.statusCode,
});
}
return response.internalError({ body: error });
}
};
};

View file

@ -0,0 +1,17 @@
/*
* 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 { IRouter, Logger } from 'src/core/server';
import { initWorkpadRoutes } from './workpad';
export interface RouteInitializerDeps {
router: IRouter;
logger: Logger;
}
export function initRoutes(deps: RouteInitializerDeps) {
initWorkpadRoutes(deps);
}

View file

@ -0,0 +1,102 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import sinon from 'sinon';
import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
loggingServiceMock,
} from 'src/core/server/mocks';
import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants';
import { initializeCreateWorkpadRoute } from './create';
import {
IRouter,
kibanaResponseFactory,
RequestHandlerContext,
RequestHandler,
} from 'src/core/server';
const mockRouteContext = ({
core: {
savedObjects: {
client: savedObjectsClientMock.create(),
},
},
} as unknown) as RequestHandlerContext;
const mockedUUID = '123abc';
const now = new Date();
const nowIso = now.toISOString();
jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc'));
describe('POST workpad', () => {
let routeHandler: RequestHandler<any, any, any>;
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
clock = sinon.useFakeTimers(now);
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
initializeCreateWorkpadRoute({
router,
logger: loggingServiceMock.create().get(),
});
routeHandler = router.post.mock.calls[0][1];
});
afterEach(() => {
clock.restore();
});
it(`returns 200 when the workpad is created`, async () => {
const mockWorkpad = {
pages: [],
};
const request = httpServerMock.createKibanaRequest({
method: 'post',
path: 'api/canvas/workpad',
body: mockWorkpad,
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(response.payload).toEqual({ ok: true });
expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith(
CANVAS_TYPE,
{
...mockWorkpad,
'@timestamp': nowIso,
'@created': nowIso,
},
{
id: `workpad-${mockedUUID}`,
}
);
});
it(`returns bad request if create is unsuccessful`, async () => {
const request = httpServerMock.createKibanaRequest({
method: 'post',
path: 'api/canvas/workpad',
body: {},
});
(mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementation(() => {
throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request');
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(400);
});
});

View file

@ -0,0 +1,57 @@
/*
* 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 {
CANVAS_TYPE,
API_ROUTE_WORKPAD,
} from '../../../../../legacy/plugins/canvas/common/lib/constants';
import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types';
import { getId } from '../../../../../legacy/plugins/canvas/public/lib/get_id';
import { WorkpadSchema } from './workpad_schema';
import { okResponse } from './ok_response';
import { catchErrorHandler } from '../catch_error_handler';
export type WorkpadAttributes = Pick<CanvasWorkpad, Exclude<keyof CanvasWorkpad, 'id'>> & {
'@timestamp': string;
'@created': string;
};
export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) {
const { router } = deps;
router.post(
{
path: `${API_ROUTE_WORKPAD}`,
validate: {
body: WorkpadSchema,
},
},
catchErrorHandler(async (context, request, response) => {
if (!request.body) {
return response.badRequest({ body: 'A workpad payload is required' });
}
const workpad = request.body as CanvasWorkpad;
const now = new Date().toISOString();
const { id, ...payload } = workpad;
await context.core.savedObjects.client.create<WorkpadAttributes>(
CANVAS_TYPE,
{
...payload,
'@timestamp': now,
'@created': now,
},
{ id: id || getId('workpad') }
);
return response.ok({
body: okResponse,
});
})
);
}

View file

@ -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.
*/
import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants';
import { initializeDeleteWorkpadRoute } from './delete';
import {
IRouter,
kibanaResponseFactory,
RequestHandlerContext,
RequestHandler,
} from 'src/core/server';
import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
loggingServiceMock,
} from 'src/core/server/mocks';
const mockRouteContext = ({
core: {
savedObjects: {
client: savedObjectsClientMock.create(),
},
},
} as unknown) as RequestHandlerContext;
describe('DELETE workpad', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeEach(() => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
initializeDeleteWorkpadRoute({
router,
logger: loggingServiceMock.create().get(),
});
routeHandler = router.delete.mock.calls[0][1];
});
it(`returns 200 ok when the workpad is deleted`, async () => {
const id = 'some-id';
const request = httpServerMock.createKibanaRequest({
method: 'delete',
path: `api/canvas/workpad/${id}`,
params: {
id,
},
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(response.payload).toEqual({ ok: true });
expect(mockRouteContext.core.savedObjects.client.delete).toBeCalledWith(CANVAS_TYPE, id);
});
it(`returns bad request if delete is unsuccessful`, async () => {
const request = httpServerMock.createKibanaRequest({
method: 'delete',
path: `api/canvas/workpad/some-id`,
params: {
id: 'some-id',
},
});
(mockRouteContext.core.savedObjects.client.delete as jest.Mock).mockImplementationOnce(() => {
throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request');
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(400);
});
});

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 { schema } from '@kbn/config-schema';
import { RouteInitializerDeps } from '../';
import {
CANVAS_TYPE,
API_ROUTE_WORKPAD,
} from '../../../../../legacy/plugins/canvas/common/lib/constants';
import { okResponse } from './ok_response';
import { catchErrorHandler } from '../catch_error_handler';
export function initializeDeleteWorkpadRoute(deps: RouteInitializerDeps) {
const { router } = deps;
router.delete(
{
path: `${API_ROUTE_WORKPAD}/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
catchErrorHandler(async (context, request, response) => {
context.core.savedObjects.client.delete(CANVAS_TYPE, request.params.id);
return response.ok({ body: okResponse });
})
);
}

View file

@ -0,0 +1,113 @@
/*
* 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 { initializeFindWorkpadsRoute } from './find';
import {
IRouter,
kibanaResponseFactory,
RequestHandlerContext,
RequestHandler,
} from 'src/core/server';
import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
loggingServiceMock,
} from 'src/core/server/mocks';
const mockRouteContext = ({
core: {
savedObjects: {
client: savedObjectsClientMock.create(),
},
},
} as unknown) as RequestHandlerContext;
describe('Find workpad', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeEach(() => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
initializeFindWorkpadsRoute({
router,
logger: loggingServiceMock.create().get(),
});
routeHandler = router.get.mock.calls[0][1];
});
it(`returns 200 with the found workpads`, async () => {
const name = 'something';
const perPage = 10000;
const mockResults = {
total: 2,
saved_objects: [
{ id: 1, attributes: { key: 'value' } },
{ id: 2, attributes: { key: 'other-value' } },
],
};
const findMock = mockRouteContext.core.savedObjects.client.find as jest.Mock;
findMock.mockResolvedValueOnce(mockResults);
const request = httpServerMock.createKibanaRequest({
method: 'get',
path: `api/canvas/workpad/find`,
query: {
name,
perPage,
},
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(findMock.mock.calls[0][0].search).toBe(`${name}* | ${name}`);
expect(findMock.mock.calls[0][0].perPage).toBe(perPage);
expect(response.payload).toMatchInlineSnapshot(`
Object {
"total": 2,
"workpads": Array [
Object {
"id": 1,
"key": "value",
},
Object {
"id": 2,
"key": "other-value",
},
],
}
`);
});
it(`returns 200 with empty results on error`, async () => {
(mockRouteContext.core.savedObjects.client.find as jest.Mock).mockImplementationOnce(() => {
throw new Error('generic error');
});
const request = httpServerMock.createKibanaRequest({
method: 'get',
path: `api/canvas/workpad/find`,
query: {
name: 'something',
perPage: 1000,
},
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(response.payload).toMatchInlineSnapshot(`
Object {
"total": 0,
"workpads": Array [],
}
`);
});
});

View file

@ -0,0 +1,60 @@
/*
* 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';
import { SavedObjectAttributes } from 'src/core/server';
import { RouteInitializerDeps } from '../';
import {
CANVAS_TYPE,
API_ROUTE_WORKPAD,
} from '../../../../../legacy/plugins/canvas/common/lib/constants';
export function initializeFindWorkpadsRoute(deps: RouteInitializerDeps) {
const { router } = deps;
router.get(
{
path: `${API_ROUTE_WORKPAD}/find`,
validate: {
query: schema.object({
name: schema.string(),
page: schema.maybe(schema.number()),
perPage: schema.number(),
}),
},
},
async (context, request, response) => {
const savedObjectsClient = context.core.savedObjects.client;
const { name, page, perPage } = request.query;
try {
const workpads = await savedObjectsClient.find<SavedObjectAttributes>({
type: CANVAS_TYPE,
sortField: '@timestamp',
sortOrder: 'desc',
search: name ? `${name}* | ${name}` : '*',
searchFields: ['name'],
fields: ['id', 'name', '@created', '@timestamp'],
page,
perPage,
});
return response.ok({
body: {
total: workpads.total,
workpads: workpads.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })),
},
});
} catch (error) {
return response.ok({
body: {
total: 0,
workpads: [],
},
});
}
}
);
}

View file

@ -0,0 +1,140 @@
/*
* 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 { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants';
import { initializeGetWorkpadRoute } from './get';
import {
IRouter,
kibanaResponseFactory,
RequestHandlerContext,
RequestHandler,
} from 'src/core/server';
import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
loggingServiceMock,
} from 'src/core/server/mocks';
import { workpadWithGroupAsElement } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads';
import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types';
const mockRouteContext = ({
core: {
savedObjects: {
client: savedObjectsClientMock.create(),
},
},
} as unknown) as RequestHandlerContext;
describe('GET workpad', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeEach(() => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
initializeGetWorkpadRoute({
router,
logger: loggingServiceMock.create().get(),
});
routeHandler = router.get.mock.calls[0][1];
});
it(`returns 200 when the workpad is found`, async () => {
const request = httpServerMock.createKibanaRequest({
method: 'get',
path: 'api/canvas/workpad/123',
params: {
id: '123',
},
});
const savedObjectsClient = savedObjectsClientMock.create();
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
type: CANVAS_TYPE,
attributes: { foo: true },
references: [],
});
mockRouteContext.core.savedObjects.client = savedObjectsClient;
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(response.payload).toMatchInlineSnapshot(`
Object {
"foo": true,
"id": "123",
}
`);
expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"canvas-workpad",
"123",
],
]
`);
});
it('corrects elements that should be groups', async () => {
const request = httpServerMock.createKibanaRequest({
method: 'get',
path: 'api/canvas/workpad/123',
params: {
id: '123',
},
});
const savedObjectsClient = savedObjectsClientMock.create();
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
type: CANVAS_TYPE,
attributes: workpadWithGroupAsElement as any,
references: [],
});
mockRouteContext.core.savedObjects.client = savedObjectsClient;
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
const workpad = response.payload as CanvasWorkpad;
expect(response.status).toBe(200);
expect(workpad).not.toBeUndefined();
expect(workpad.pages[0].elements.length).toBe(1);
expect(workpad.pages[0].groups.length).toBe(1);
});
it('returns 404 if the workpad is not found', async () => {
const id = '123';
const request = httpServerMock.createKibanaRequest({
method: 'get',
path: 'api/canvas/workpad/123',
params: {
id,
},
});
const savedObjectsClient = savedObjectsClientMock.create();
savedObjectsClient.get.mockImplementation(() => {
throw savedObjectsClient.errors.createGenericNotFoundError(CANVAS_TYPE, id);
});
mockRouteContext.core.savedObjects.client = savedObjectsClient;
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.payload).toMatchInlineSnapshot(`
Object {
"error": "Not Found",
"message": "Saved object [canvas-workpad/123] not found",
"statusCode": 404,
}
`);
});
});

View file

@ -0,0 +1,65 @@
/*
* 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';
import { RouteInitializerDeps } from '../';
import {
CANVAS_TYPE,
API_ROUTE_WORKPAD,
} from '../../../../../legacy/plugins/canvas/common/lib/constants';
import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types';
import { catchErrorHandler } from '../catch_error_handler';
export type WorkpadAttributes = Pick<CanvasWorkpad, Exclude<keyof CanvasWorkpad, 'id'>> & {
'@timestamp': string;
'@created': string;
};
export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) {
const { router } = deps;
router.get(
{
path: `${API_ROUTE_WORKPAD}/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
catchErrorHandler(async (context, request, response) => {
const workpad = await context.core.savedObjects.client.get<WorkpadAttributes>(
CANVAS_TYPE,
request.params.id
);
if (
// not sure if we need to be this defensive
workpad.type === 'canvas-workpad' &&
workpad.attributes &&
workpad.attributes.pages &&
workpad.attributes.pages.length
) {
workpad.attributes.pages.forEach(page => {
const elements = (page.elements || []).filter(
({ id: pageId }) => !pageId.startsWith('group')
);
const groups = (page.groups || []).concat(
(page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group'))
);
page.elements = elements;
page.groups = groups;
});
}
return response.ok({
body: {
id: workpad.id,
...workpad.attributes,
},
});
})
);
}

View file

@ -0,0 +1,21 @@
/*
* 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 { initializeFindWorkpadsRoute } from './find';
import { initializeGetWorkpadRoute } from './get';
import { initializeCreateWorkpadRoute } from './create';
import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update';
import { initializeDeleteWorkpadRoute } from './delete';
export function initWorkpadRoutes(deps: RouteInitializerDeps) {
initializeFindWorkpadsRoute(deps);
initializeGetWorkpadRoute(deps);
initializeCreateWorkpadRoute(deps);
initializeUpdateWorkpadRoute(deps);
initializeUpdateWorkpadAssetsRoute(deps);
initializeDeleteWorkpadRoute(deps);
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const okResponse = {
ok: true,
};

View file

@ -0,0 +1,223 @@
/*
* 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 sinon from 'sinon';
import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants';
import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update';
import {
IRouter,
kibanaResponseFactory,
RequestHandlerContext,
RequestHandler,
} from 'src/core/server';
import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
loggingServiceMock,
} from 'src/core/server/mocks';
import { workpads } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads';
import { okResponse } from './ok_response';
const mockRouteContext = ({
core: {
savedObjects: {
client: savedObjectsClientMock.create(),
},
},
} as unknown) as RequestHandlerContext;
const workpad = workpads[0];
const now = new Date();
const nowIso = now.toISOString();
jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc'));
describe('PUT workpad', () => {
let routeHandler: RequestHandler<any, any, any>;
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
clock = sinon.useFakeTimers(now);
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
initializeUpdateWorkpadRoute({
router,
logger: loggingServiceMock.create().get(),
});
routeHandler = router.put.mock.calls[0][1];
});
afterEach(() => {
jest.resetAllMocks();
clock.restore();
});
it(`returns 200 ok when the workpad is updated`, async () => {
const updatedWorkpad = { name: 'new name' };
const { id, ...workpadAttributes } = workpad;
const request = httpServerMock.createKibanaRequest({
method: 'put',
path: `api/canvas/workpad/${id}`,
params: {
id,
},
body: updatedWorkpad,
});
const savedObjectsClient = savedObjectsClientMock.create();
savedObjectsClient.get.mockResolvedValueOnce({
id,
type: CANVAS_TYPE,
attributes: workpadAttributes as any,
references: [],
});
mockRouteContext.core.savedObjects.client = savedObjectsClient;
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(response.payload).toEqual(okResponse);
expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith(
CANVAS_TYPE,
{
...workpadAttributes,
...updatedWorkpad,
'@timestamp': nowIso,
'@created': workpad['@created'],
},
{
overwrite: true,
id,
}
);
});
it(`returns not found if existing workpad is not found`, async () => {
const request = httpServerMock.createKibanaRequest({
method: 'put',
path: 'api/canvas/workpad/some-id',
params: {
id: 'not-found',
},
body: {},
});
(mockRouteContext.core.savedObjects.client.get as jest.Mock).mockImplementationOnce(() => {
throw mockRouteContext.core.savedObjects.client.errors.createGenericNotFoundError(
'not found'
);
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(404);
});
it(`returns bad request if the write fails`, async () => {
const request = httpServerMock.createKibanaRequest({
method: 'put',
path: 'api/canvas/workpad/some-id',
params: {
id: 'some-id',
},
body: {},
});
const savedObjectsClient = savedObjectsClientMock.create();
savedObjectsClient.get.mockResolvedValueOnce({
id: 'some-id',
type: CANVAS_TYPE,
attributes: {},
references: [],
});
mockRouteContext.core.savedObjects.client = savedObjectsClient;
(mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementationOnce(() => {
throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request');
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(400);
});
});
describe('update assets', () => {
let routeHandler: RequestHandler<any, any, any>;
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
clock = sinon.useFakeTimers(now);
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
initializeUpdateWorkpadAssetsRoute({
router,
logger: loggingServiceMock.create().get(),
});
routeHandler = router.put.mock.calls[0][1];
});
afterEach(() => {
clock.restore();
});
it('updates assets', async () => {
const { id, ...attributes } = workpad;
const assets = {
'asset-1': {
'@created': new Date().toISOString(),
id: 'asset-1',
type: 'asset',
value: 'some-url-encoded-asset',
},
'asset-2': {
'@created': new Date().toISOString(),
id: 'asset-2',
type: 'asset',
value: 'some-other asset',
},
};
const request = httpServerMock.createKibanaRequest({
method: 'put',
path: 'api/canvas/workpad-assets/some-id',
params: {
id,
},
body: assets,
});
(mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValueOnce({
id,
type: CANVAS_TYPE,
attributes: attributes as any,
references: [],
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith(
CANVAS_TYPE,
{
...attributes,
'@timestamp': nowIso,
assets,
},
{
id,
overwrite: true,
}
);
});
});

View file

@ -0,0 +1,129 @@
/*
* 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, TypeOf } from '@kbn/config-schema';
import { omit } from 'lodash';
import { KibanaResponseFactory } from 'src/core/server';
import { SavedObjectsClientContract } from 'src/core/server';
import { RouteInitializerDeps } from '../';
import {
CANVAS_TYPE,
API_ROUTE_WORKPAD,
API_ROUTE_WORKPAD_STRUCTURES,
API_ROUTE_WORKPAD_ASSETS,
} from '../../../../../legacy/plugins/canvas/common/lib/constants';
import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types';
import { WorkpadSchema, WorkpadAssetSchema } from './workpad_schema';
import { okResponse } from './ok_response';
import { catchErrorHandler } from '../catch_error_handler';
export type WorkpadAttributes = Pick<CanvasWorkpad, Exclude<keyof CanvasWorkpad, 'id'>> & {
'@timestamp': string;
'@created': string;
};
const AssetsRecordSchema = schema.recordOf(schema.string(), WorkpadAssetSchema);
const AssetPayloadSchema = schema.object({
assets: AssetsRecordSchema,
});
const workpadUpdateHandler = async (
payload: TypeOf<typeof WorkpadSchema> | TypeOf<typeof AssetPayloadSchema>,
id: string,
savedObjectsClient: SavedObjectsClientContract,
response: KibanaResponseFactory
) => {
const now = new Date().toISOString();
const workpadObject = await savedObjectsClient.get<WorkpadAttributes>(CANVAS_TYPE, id);
await savedObjectsClient.create<WorkpadAttributes>(
CANVAS_TYPE,
{
...workpadObject.attributes,
...omit(payload, 'id'), // never write the id property
'@timestamp': now, // always update the modified time
'@created': workpadObject.attributes['@created'], // ensure created is not modified
},
{ overwrite: true, id }
);
return response.ok({
body: okResponse,
});
};
export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) {
const { router } = deps;
// TODO: This route is likely deprecated and everything is using the workpad_structures
// path instead. Investigate further.
router.put(
{
path: `${API_ROUTE_WORKPAD}/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
body: WorkpadSchema,
},
},
catchErrorHandler(async (context, request, response) => {
return workpadUpdateHandler(
request.body,
request.params.id,
context.core.savedObjects.client,
response
);
})
);
router.put(
{
path: `${API_ROUTE_WORKPAD_STRUCTURES}/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
body: WorkpadSchema,
},
},
catchErrorHandler(async (context, request, response) => {
return workpadUpdateHandler(
request.body,
request.params.id,
context.core.savedObjects.client,
response
);
})
);
}
export function initializeUpdateWorkpadAssetsRoute(deps: RouteInitializerDeps) {
const { router } = deps;
router.put(
{
path: `${API_ROUTE_WORKPAD_ASSETS}/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
// ToDo: Currently the validation must be a schema.object
// Because we don't know what keys the assets will have, we have to allow
// unknowns and then validate in the handler
body: schema.object({}, { allowUnknowns: true }),
},
},
async (context, request, response) => {
return workpadUpdateHandler(
{ assets: AssetsRecordSchema.validate(request.body) },
request.params.id,
context.core.savedObjects.client,
response
);
}
);
}

View file

@ -0,0 +1,65 @@
/*
* 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 WorkpadElementSchema = schema.object({
expression: schema.string(),
filter: schema.maybe(schema.nullable(schema.string())),
id: schema.string(),
position: PositionSchema,
});
export const WorkpadPageSchema = schema.object({
elements: schema.arrayOf(WorkpadElementSchema),
groups: schema.arrayOf(
schema.object({
id: schema.string(),
position: PositionSchema,
})
),
id: schema.string(),
style: schema.recordOf(schema.string(), schema.string()),
transition: schema.maybe(
schema.oneOf([
schema.object({}),
schema.object({
name: schema.string(),
}),
])
),
});
export const WorkpadAssetSchema = schema.object({
'@created': schema.string(),
id: schema.string(),
type: schema.string(),
value: schema.string(),
});
export const WorkpadSchema = schema.object({
'@created': schema.maybe(schema.string()),
'@timestamp': schema.maybe(schema.string()),
assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)),
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(WorkpadPageSchema),
width: schema.number(),
});