From 45cc8d2aa533073a95b5805610ceba65fd69b8d4 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 18 Sep 2019 12:56:17 -0400 Subject: [PATCH] [7.x] Custom space avatar images (#45148) (#46019) * First changes for avatar images * Added the ability to have custom images for space avatars * Partial changes as requested by reviewers * Final commit for space avatar images PR * Wrapping avatar file name * Colour picker always enabled, to allow background change for transparent svgs * All the changes requested in the last review * Fixes the type_check test errors * Fixing the rendering errors for space pages * Another batch of changes as requested by review * Some more snapshot tests * Last batch of changes * Fixed the type_check test * API doc updates * Removed comment * Removed imageUrl from state Co-authored-by: Larry Gregory Co-authored-by: Elastic Machine --- docs/api/spaces-management/get.asciidoc | 3 +- docs/api/spaces-management/get_all.asciidoc | 5 +- docs/api/spaces-management/post.asciidoc | 7 +- docs/api/spaces-management/put.asciidoc | 7 +- .../plugins/spaces/common/lib/dataurl.ts | 25 ++++ .../plugins/spaces/common/model/space.ts | 1 + .../plugins/spaces/common/space_attributes.ts | 15 +++ x-pack/legacy/plugins/spaces/mappings.json | 4 + .../__snapshots__/space_avatar.test.tsx.snap | 3 + .../spaces/public/components/space_avatar.tsx | 2 + .../customize_space_avatar.test.tsx.snap | 27 ++++ .../customize_space_avatar.tsx | 120 +++++++++++++++++- .../edit_space/manage_space_page.tsx | 2 + .../spaces_grid_pages.test.tsx.snap | 3 + .../spaces/server/lib/space_schema.test.ts | 29 +++++ .../plugins/spaces/server/lib/space_schema.ts | 3 + .../apis/spaces/space_attributes.ts | 45 +++++++ 17 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 x-pack/legacy/plugins/spaces/common/lib/dataurl.ts diff --git a/docs/api/spaces-management/get.asciidoc b/docs/api/spaces-management/get.asciidoc index db5358cc28a9..569117e866d2 100644 --- a/docs/api/spaces-management/get.asciidoc +++ b/docs/api/spaces-management/get.asciidoc @@ -32,6 +32,7 @@ The API returns the following: "description" : "This is the Marketing Space", "color": "#aabbcc", "initials": "MK", - "disabledFeatures": [] + "disabledFeatures": [], + "imageUrl": "" } -------------------------------------------------- \ No newline at end of file diff --git a/docs/api/spaces-management/get_all.asciidoc b/docs/api/spaces-management/get_all.asciidoc index 52c9a78be262..6f683b864e14 100644 --- a/docs/api/spaces-management/get_all.asciidoc +++ b/docs/api/spaces-management/get_all.asciidoc @@ -32,6 +32,7 @@ The API returns the following: "name": "Default", "description" : "This is the Default Space", "disabledFeatures": [], + "imageUrl": "", "_reserved": true }, { @@ -40,13 +41,15 @@ The API returns the following: "description" : "This is the Marketing Space", "color": "#aabbcc", "disabledFeatures": ["apm"], - "initials": "MK" + "initials": "MK", + "imageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSU", }, { "id": "sales", "name": "Sales", "initials": "MK", "disabledFeatures": ["discover", "timelion"], + "imageUrl": "" }, ] -------------------------------------------------- diff --git a/docs/api/spaces-management/post.asciidoc b/docs/api/spaces-management/post.asciidoc index 2fddba1d5190..ea8311ea8213 100644 --- a/docs/api/spaces-management/post.asciidoc +++ b/docs/api/spaces-management/post.asciidoc @@ -33,6 +33,10 @@ experimental["The underlying Spaces concepts are stable, but the APIs for managi `color`:: (Optional, string) Specifies the hexadecimal color code used in the space avatar. By default, the color is automatically generated from the space name. + +`imageUrl`:: + (Optional, string) Specifies the data-url encoded image to display in the space avatar. If specified, `initials` will not be displayed, and the `color` will be visible as the background color for transparent images. + For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images. [[spaces-api-post-response-codes]] ==== Response codes @@ -52,7 +56,8 @@ POST /api/spaces/space "description" : "This is the Marketing Space", "color": "#aabbcc", "initials": "MK", - "disabledFeatures": ["timelion"] + "disabledFeatures": ["timelion"], + "imageUrl": "data:image/png;base64,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" } -------------------------------------------------- // KIBANA diff --git a/docs/api/spaces-management/put.asciidoc b/docs/api/spaces-management/put.asciidoc index 55e48488359c..113fdbce6a90 100644 --- a/docs/api/spaces-management/put.asciidoc +++ b/docs/api/spaces-management/put.asciidoc @@ -34,6 +34,10 @@ experimental["The underlying Spaces concepts are stable, but the APIs for managi `color`:: (Optional, string) Specifies the hexadecimal color code used in the space avatar. By default, the color is automatically generated from the space name. +`imageUrl`:: + (Optional, string) Specifies the data-url encoded image to display in the space avatar. If specified, `initials` will not be displayed, and the `color` will be visible as the background color for transparent images. + For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images. + [[spaces-api-put-response-codes]] ==== Response codes @@ -52,7 +56,8 @@ PUT /api/spaces/space/marketing "description" : "This is the Marketing Space", "color": "#aabbcc", "initials": "MK", - "disabledFeatures": [] + "disabledFeatures": [], + "imageUrl": "" } -------------------------------------------------- // KIBANA diff --git a/x-pack/legacy/plugins/spaces/common/lib/dataurl.ts b/x-pack/legacy/plugins/spaces/common/lib/dataurl.ts new file mode 100644 index 000000000000..c7655d19f808 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/common/lib/dataurl.ts @@ -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 { fromByteArray } from 'base64-js'; + +export const imageTypes = ['image/svg+xml', 'image/jpeg', 'image/png', 'image/gif']; + +export function encode(data: any | null, type = 'text/plain') { + // use FileReader if it's available, like in the browser + if (FileReader) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = err => reject(err); + reader.readAsDataURL(data); + }); + } + + // otherwise fall back to fromByteArray + // note: Buffer doesn't seem to correctly base64 encode binary data + return Promise.resolve(`data:${type};base64,${fromByteArray(data)}`); +} diff --git a/x-pack/legacy/plugins/spaces/common/model/space.ts b/x-pack/legacy/plugins/spaces/common/model/space.ts index 4fc7e150f890..c44ce41ec51c 100644 --- a/x-pack/legacy/plugins/spaces/common/model/space.ts +++ b/x-pack/legacy/plugins/spaces/common/model/space.ts @@ -12,4 +12,5 @@ export interface Space { initials?: string; disabledFeatures: string[]; _reserved?: boolean; + imageUrl?: string; } diff --git a/x-pack/legacy/plugins/spaces/common/space_attributes.ts b/x-pack/legacy/plugins/spaces/common/space_attributes.ts index c73a4b5aca7a..f943dcf4af10 100644 --- a/x-pack/legacy/plugins/spaces/common/space_attributes.ts +++ b/x-pack/legacy/plugins/spaces/common/space_attributes.ts @@ -52,3 +52,18 @@ export function getSpaceInitials(space: Partial = {}) { return words.map(word => word.substring(0, 1)).join(''); } + +/** + * Determines the avatar image for the provided space. + * + * @param {Space} space + */ +export function getSpaceImageUrl(space: Partial = {}) { + const { imageUrl } = space; + + if (imageUrl) { + return imageUrl; + } + + return ''; +} diff --git a/x-pack/legacy/plugins/spaces/mappings.json b/x-pack/legacy/plugins/spaces/mappings.json index 6d3aa3556a9e..dc73dc287188 100644 --- a/x-pack/legacy/plugins/spaces/mappings.json +++ b/x-pack/legacy/plugins/spaces/mappings.json @@ -22,6 +22,10 @@ "disabledFeatures": { "type": "keyword" }, + "imageUrl": { + "type": "text", + "index": false + }, "_reserved": { "type": "boolean" } diff --git a/x-pack/legacy/plugins/spaces/public/components/__snapshots__/space_avatar.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/components/__snapshots__/space_avatar.test.tsx.snap index 9b6336ad6444..c07689405973 100644 --- a/x-pack/legacy/plugins/spaces/public/components/__snapshots__/space_avatar.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/components/__snapshots__/space_avatar.test.tsx.snap @@ -15,6 +15,7 @@ exports[`removes aria-label when instructed not to announce the space name 1`] = aria-label="" color="#BFA180" data-test-subj="space-avatar-" + imageUrl="" initials="" initialsLength={2} name="" @@ -43,6 +44,7 @@ exports[`renders with a space name entirely made of whitespace 1`] = ` ; @@ -37,6 +38,7 @@ export const SpaceAvatar: SFC = (props: Props) => { initialsLength={MAX_SPACE_INITIALS} initials={getSpaceInitials(space)} color={isValidHex(spaceColor) ? spaceColor : ''} + imageUrl={getSpaceImageUrl(space)} {...rest} /> ); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap index 646d2ed440b6..97e4bae437cf 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap @@ -14,6 +14,7 @@ exports[`renders without crashing 1`] = ` > + + + + `; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx index 519b0cc8b6fb..2f179083d7b9 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx @@ -4,9 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiColorPicker, EuiFieldText, EuiSpacer, EuiFormRow, isValidHex } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component } from 'react'; +import { + EuiColorPicker, + EuiFieldText, + EuiFlexItem, + EuiFormRow, + // @ts-ignore (elastic/eui#1262) EuiFilePicker is not exported yet + EuiFilePicker, + EuiButton, + EuiSpacer, + isValidHex, +} from '@elastic/eui'; +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; + +import { encode, imageTypes } from '../../../../../common/lib/dataurl'; + import { MAX_SPACE_INITIALS } from '../../../../../common/constants'; import { Space } from '../../../../../common/model/space'; import { getSpaceColor, getSpaceInitials } from '../../../../../common/space_attributes'; @@ -32,6 +45,63 @@ class CustomizeSpaceAvatarUI extends Component { }; } + private storeImageChanges(imageUrl: string) { + this.props.onChange({ + ...this.props.space, + imageUrl, + }); + } + + // + // images below 64x64 pixels are left untouched + // images above that threshold are resized + // + + private handleImageUpload = (imgUrl: string) => { + const thisInstance = this; + const image = new Image(); + image.addEventListener( + 'load', + function() { + const MAX_IMAGE_SIZE = 64; + const imgDimx = image.width; + const imgDimy = image.height; + if (imgDimx <= MAX_IMAGE_SIZE && imgDimy <= MAX_IMAGE_SIZE) { + thisInstance.storeImageChanges(imgUrl); + } else { + const imageCanvas = document.createElement('canvas'); + const canvasContext = imageCanvas.getContext('2d'); + if (imgDimx >= imgDimy) { + imageCanvas.width = MAX_IMAGE_SIZE; + imageCanvas.height = Math.floor((imgDimy * MAX_IMAGE_SIZE) / imgDimx); + if (canvasContext) { + canvasContext.drawImage(image, 0, 0, imageCanvas.width, imageCanvas.height); + const resizedImageUrl = imageCanvas.toDataURL(); + thisInstance.storeImageChanges(resizedImageUrl); + } + } else { + imageCanvas.height = MAX_IMAGE_SIZE; + imageCanvas.width = Math.floor((imgDimx * MAX_IMAGE_SIZE) / imgDimy); + if (canvasContext) { + canvasContext.drawImage(image, 0, 0, imageCanvas.width, imageCanvas.height); + const resizedImageUrl = imageCanvas.toDataURL(); + thisInstance.storeImageChanges(resizedImageUrl); + } + } + } + }, + false + ); + image.src = imgUrl; + }; + + private onFileUpload = (files: File[]) => { + const [file] = files; + if (imageTypes.indexOf(file.type) > -1) { + encode(file).then((dataurl: string) => this.handleImageUpload(dataurl)); + } + }; + public render() { const { space, intl } = this.props; @@ -55,6 +125,7 @@ class CustomizeSpaceAvatarUI extends Component { // without defaulting to the derived initials provided by `getSpaceInitials` value={initialsHasFocus ? pendingInitials || '' : getSpaceInitials(space)} onChange={this.onInitialsChange} + disabled={this.props.space.imageUrl && this.props.space.imageUrl !== '' ? true : false} /> @@ -71,10 +142,55 @@ class CustomizeSpaceAvatarUI extends Component { isInvalid={isInvalidSpaceColor} /> + + {this.filePickerOrImage()} ); } + private removeImageUrl() { + this.props.onChange({ + ...this.props.space, + imageUrl: '', + }); + } + + public filePickerOrImage() { + const { intl } = this.props; + + if (!this.props.space.imageUrl) { + return ( + + + + ); + } else { + return ( + + this.removeImageUrl()} color="danger" iconType="trash"> + {intl.formatMessage({ + id: 'xpack.spaces.management.customizeSpaceAvatar.removeImage', + defaultMessage: 'Remove custom image', + })} + + + ); + } + } + public initialsInputRef = (ref: HTMLInputElement) => { if (ref) { this.initialsRef = ref; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx index c629e01fe85a..7a3fea0d76a3 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx @@ -334,6 +334,7 @@ class ManageSpacePageUI extends Component { initials, color, disabledFeatures = [], + imageUrl, } = this.state.space; const params = { @@ -343,6 +344,7 @@ class ManageSpacePageUI extends Component { initials, color, disabledFeatures, + imageUrl, }; let action; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap index 0150b88923d1..fcf0e992b2be 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap @@ -145,6 +145,7 @@ Array [ { ); }); }); + +describe('#imageUrl', () => { + test('is optional', () => { + const result = spaceSchema.validate({ + ...defaultProperties, + imageUrl: undefined, + }); + expect(result.error).toBeNull(); + }); + + test(`must start with data:image`, () => { + const result = spaceSchema.validate({ + ...defaultProperties, + imageUrl: 'notValid', + }); + expect(result.error).toMatchInlineSnapshot( + `[ValidationError: child "imageUrl" fails because ["imageUrl" with value "notValid" fails to match the Image URL should start with 'data:image' pattern]]` + ); + }); + + test(`checking that a valid image is accepted as imageUrl`, () => { + const result = spaceSchema.validate({ + ...defaultProperties, + imageUrl: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTnU1rJkAAAB3klEQVRYR+2WzUrDQBCARzwqehE8ir1WPfgqRRA1bePBXgpe/MGCB9/Aiw+j+ASCB6kotklaEwW1F0WwNSaps9lV69awGzBpDzt8pJP9mXxsmk3ABH2oUEIilJAIJSRCCYlQQiKUkIh4QgY5agZodVjBowFrBktWQzDBU2ykiYaDuQpCYgnl3QunGzM6Z6YF+b5SkcgK1UH/aLbYReQiYL9d9/o+XFop5IU0Vl4uapAzoXC3eEBPw9vH1/wT6Vs2otPSkoH/IZzlzO/TU2vgQm8nl69Hp0H7nZ4OXogLJSSKBIUC3w88n+Ueyfv56fVZnqCQNVnCHbLrkV0Gd2d+GNkglsk438dhaTxloZDutV4wb06Vf40JcWZ2sMttPpE8NaHGeBnzIAhwPXqHseVB11EyLD0hxLUeaYud2a3B0g3k7GyFtrhX7F2RqhC+yV3jgTb2Rqdqf7/kUxYiWBOlTtXxfPJEtc8b5thGb+8AhL4ohnCNqQjZ2T2+K5rnw2M6KwEhKNDSGM3pTdxjhDgLbHkw/v/zw4AiPuSsfMzAiTidKxiF/ArpFqyzK8SMOlkwvloUMYRCtNvZLWeuIomd2Za/WZS4QomjhEQoIRFKSIQSEqGERAyfEH4YDBFQ/ARU6BiBxCAIQQAAAABJRU5ErkJggg==', + }); + expect(result.error).toBeNull(); + }); +}); diff --git a/x-pack/legacy/plugins/spaces/server/lib/space_schema.ts b/x-pack/legacy/plugins/spaces/server/lib/space_schema.ts index c298e62b9bf0..2b8175b09794 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/space_schema.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/space_schema.ts @@ -19,4 +19,7 @@ export const spaceSchema = Joi.object({ .items(Joi.string()) .default([]), _reserved: Joi.boolean(), + imageUrl: Joi.string() + .allow('') + .regex(/^data:image.*$/, `Image URL should start with 'data:image'`), }).default(); diff --git a/x-pack/test/api_integration/apis/spaces/space_attributes.ts b/x-pack/test/api_integration/apis/spaces/space_attributes.ts index 1f9c0315f0e1..b3863cd82835 100644 --- a/x-pack/test/api_integration/apis/spaces/space_attributes.ts +++ b/x-pack/test/api_integration/apis/spaces/space_attributes.ts @@ -27,5 +27,50 @@ export default function({ getService }: FtrProviderContext) { color: '#aaBB78', }); }); + + it('should allow a space to be created with an avatar image', async () => { + await supertest + .post('/api/spaces/space') + .set('kbn-xsrf', 'xxx') + .send({ + id: 'api-test-space2', + name: 'Space with image', + disabledFeatures: [], + color: '#cafeba', + imageUrl: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTnU1rJkAAAB3klEQVRYR+2WzUrDQBCARzwqehE8ir1WPfgqRRA1bePBXgpe/MGCB9/Aiw+j+ASCB6kotklaEwW1F0WwNSaps9lV69awGzBpDzt8pJP9mXxsmk3ABH2oUEIilJAIJSRCCYlQQiKUkIh4QgY5agZodVjBowFrBktWQzDBU2ykiYaDuQpCYgnl3QunGzM6Z6YF+b5SkcgK1UH/aLbYReQiYL9d9/o+XFop5IU0Vl4uapAzoXC3eEBPw9vH1/wT6Vs2otPSkoH/IZzlzO/TU2vgQm8nl69Hp0H7nZ4OXogLJSSKBIUC3w88n+Ueyfv56fVZnqCQNVnCHbLrkV0Gd2d+GNkglsk438dhaTxloZDutV4wb06Vf40JcWZ2sMttPpE8NaHGeBnzIAhwPXqHseVB11EyLD0hxLUeaYud2a3B0g3k7GyFtrhX7F2RqhC+yV3jgTb2Rqdqf7/kUxYiWBOlTtXxfPJEtc8b5thGb+8AhL4ohnCNqQjZ2T2+K5rnw2M6KwEhKNDSGM3pTdxjhDgLbHkw/v/zw4AiPuSsfMzAiTidKxiF/ArpFqyzK8SMOlkwvloUMYRCtNvZLWeuIomd2Za/WZS4QomjhEQoIRFKSIQSEqGERAyfEH4YDBFQ/ARU6BiBxCAIQQAAAABJRU5ErkJggg==', + }) + .expect(200, { + id: 'api-test-space2', + name: 'Space with image', + disabledFeatures: [], + color: '#cafeba', + imageUrl: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTnU1rJkAAAB3klEQVRYR+2WzUrDQBCARzwqehE8ir1WPfgqRRA1bePBXgpe/MGCB9/Aiw+j+ASCB6kotklaEwW1F0WwNSaps9lV69awGzBpDzt8pJP9mXxsmk3ABH2oUEIilJAIJSRCCYlQQiKUkIh4QgY5agZodVjBowFrBktWQzDBU2ykiYaDuQpCYgnl3QunGzM6Z6YF+b5SkcgK1UH/aLbYReQiYL9d9/o+XFop5IU0Vl4uapAzoXC3eEBPw9vH1/wT6Vs2otPSkoH/IZzlzO/TU2vgQm8nl69Hp0H7nZ4OXogLJSSKBIUC3w88n+Ueyfv56fVZnqCQNVnCHbLrkV0Gd2d+GNkglsk438dhaTxloZDutV4wb06Vf40JcWZ2sMttPpE8NaHGeBnzIAhwPXqHseVB11EyLD0hxLUeaYud2a3B0g3k7GyFtrhX7F2RqhC+yV3jgTb2Rqdqf7/kUxYiWBOlTtXxfPJEtc8b5thGb+8AhL4ohnCNqQjZ2T2+K5rnw2M6KwEhKNDSGM3pTdxjhDgLbHkw/v/zw4AiPuSsfMzAiTidKxiF/ArpFqyzK8SMOlkwvloUMYRCtNvZLWeuIomd2Za/WZS4QomjhEQoIRFKSIQSEqGERAyfEH4YDBFQ/ARU6BiBxCAIQQAAAABJRU5ErkJggg==', + }); + }); + + it('creating a space with an invalid image fails', async () => { + await supertest + .post('/api/spaces/space') + .set('kbn-xsrf', 'xxx') + .send({ + id: 'api-test-space3', + name: 'Space with invalid image', + disabledFeatures: [], + color: '#cafeba', + imageUrl: 'invalidImage', + }) + .expect(400, { + error: 'Bad Request', + message: + 'child "imageUrl" fails because ["imageUrl" with value "invalidImage" fails to match the Image URL should start with \'data:image\' pattern]', + statusCode: 400, + validation: { + keys: ['imageUrl'], + source: 'payload', + }, + }); + }); }); }