* 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 <larry.gregory@elastic.co> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
927204cda9
commit
45cc8d2aa5
|
@ -32,6 +32,7 @@ The API returns the following:
|
|||
"description" : "This is the Marketing Space",
|
||||
"color": "#aabbcc",
|
||||
"initials": "MK",
|
||||
"disabledFeatures": []
|
||||
"disabledFeatures": [],
|
||||
"imageUrl": ""
|
||||
}
|
||||
--------------------------------------------------
|
|
@ -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": ""
|
||||
},
|
||||
]
|
||||
--------------------------------------------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
25
x-pack/legacy/plugins/spaces/common/lib/dataurl.ts
Normal file
25
x-pack/legacy/plugins/spaces/common/lib/dataurl.ts
Normal 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 { 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<string>((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)}`);
|
||||
}
|
|
@ -12,4 +12,5 @@ export interface Space {
|
|||
initials?: string;
|
||||
disabledFeatures: string[];
|
||||
_reserved?: boolean;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
|
|
@ -52,3 +52,18 @@ export function getSpaceInitials(space: Partial<Space> = {}) {
|
|||
|
||||
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<Space> = {}) {
|
||||
const { imageUrl } = space;
|
||||
|
||||
if (imageUrl) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -22,6 +22,10 @@
|
|||
"disabledFeatures": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": "text",
|
||||
"index": false
|
||||
},
|
||||
"_reserved": {
|
||||
"type": "boolean"
|
||||
}
|
||||
|
|
|
@ -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`] = `
|
|||
<EuiAvatar
|
||||
color="#DB1374"
|
||||
data-test-subj="space-avatar-"
|
||||
imageUrl=""
|
||||
initials=""
|
||||
initialsLength={2}
|
||||
name=""
|
||||
|
@ -55,6 +57,7 @@ exports[`renders without crashing 1`] = `
|
|||
<EuiAvatar
|
||||
color="#BFA180"
|
||||
data-test-subj="space-avatar-"
|
||||
imageUrl=""
|
||||
initials=""
|
||||
initialsLength={2}
|
||||
name=""
|
||||
|
|
|
@ -8,6 +8,7 @@ import { EuiAvatar, isValidHex } from '@elastic/eui';
|
|||
import React, { SFC } from 'react';
|
||||
import { getSpaceColor, getSpaceInitials, MAX_SPACE_INITIALS } from '../../common';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { getSpaceImageUrl } from '../../common/space_attributes';
|
||||
|
||||
interface Props {
|
||||
space: Partial<Space>;
|
||||
|
@ -37,6 +38,7 @@ export const SpaceAvatar: SFC<Props> = (props: Props) => {
|
|||
initialsLength={MAX_SPACE_INITIALS}
|
||||
initials={getSpaceInitials(space)}
|
||||
color={isValidHex(spaceColor) ? spaceColor : ''}
|
||||
imageUrl={getSpaceImageUrl(space)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ exports[`renders without crashing 1`] = `
|
|||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
disabled={false}
|
||||
fullWidth={false}
|
||||
inputRef={[Function]}
|
||||
isLoading={false}
|
||||
|
@ -40,5 +41,31 @@ exports[`renders without crashing 1`] = `
|
|||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Custom image"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFilePicker
|
||||
accept={
|
||||
Array [
|
||||
"image/svg+xml",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
]
|
||||
}
|
||||
compressed={false}
|
||||
display="default"
|
||||
initialPromptText="Select image file"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</form>
|
||||
`;
|
||||
|
|
|
@ -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<Props, State> {
|
|||
};
|
||||
}
|
||||
|
||||
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<Props, State> {
|
|||
// 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}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
|
@ -71,10 +142,55 @@ class CustomizeSpaceAvatarUI extends Component<Props, State> {
|
|||
isInvalid={isInvalidSpaceColor}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
{this.filePickerOrImage()}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
private removeImageUrl() {
|
||||
this.props.onChange({
|
||||
...this.props.space,
|
||||
imageUrl: '',
|
||||
});
|
||||
}
|
||||
|
||||
public filePickerOrImage() {
|
||||
const { intl } = this.props;
|
||||
|
||||
if (!this.props.space.imageUrl) {
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={intl.formatMessage({
|
||||
id: 'xpack.spaces.management.customizeSpaceAvatar.imageUrl',
|
||||
defaultMessage: 'Custom image',
|
||||
})}
|
||||
>
|
||||
<EuiFilePicker
|
||||
display="default"
|
||||
initialPromptText={intl.formatMessage({
|
||||
id: 'xpack.spaces.management.customizeSpaceAvatar.selectImageUrl',
|
||||
defaultMessage: 'Select image file',
|
||||
})}
|
||||
onChange={this.onFileUpload}
|
||||
accept={imageTypes}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiButton onClick={() => this.removeImageUrl()} color="danger" iconType="trash">
|
||||
{intl.formatMessage({
|
||||
id: 'xpack.spaces.management.customizeSpaceAvatar.removeImage',
|
||||
defaultMessage: 'Remove custom image',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public initialsInputRef = (ref: HTMLInputElement) => {
|
||||
if (ref) {
|
||||
this.initialsRef = ref;
|
||||
|
|
|
@ -334,6 +334,7 @@ class ManageSpacePageUI extends Component<Props, State> {
|
|||
initials,
|
||||
color,
|
||||
disabledFeatures = [],
|
||||
imageUrl,
|
||||
} = this.state.space;
|
||||
|
||||
const params = {
|
||||
|
@ -343,6 +344,7 @@ class ManageSpacePageUI extends Component<Props, State> {
|
|||
initials,
|
||||
color,
|
||||
disabledFeatures,
|
||||
imageUrl,
|
||||
};
|
||||
|
||||
let action;
|
||||
|
|
|
@ -145,6 +145,7 @@ Array [
|
|||
<EuiAvatar
|
||||
color="#461A0A"
|
||||
data-test-subj="space-avatar-default"
|
||||
imageUrl=""
|
||||
initials="D"
|
||||
initialsLength={2}
|
||||
name="Default"
|
||||
|
@ -186,6 +187,7 @@ Array [
|
|||
<EuiAvatar
|
||||
color="#BFA180"
|
||||
data-test-subj="space-avatar-custom-1"
|
||||
imageUrl=""
|
||||
initials="C1"
|
||||
initialsLength={2}
|
||||
name="Custom 1"
|
||||
|
@ -230,6 +232,7 @@ Array [
|
|||
<EuiAvatar
|
||||
color="#ABCDEF"
|
||||
data-test-subj="space-avatar-custom-2"
|
||||
imageUrl=""
|
||||
initials="LG"
|
||||
initialsLength={2}
|
||||
name="Custom 2"
|
||||
|
|
|
@ -136,3 +136,32 @@ describe('#color', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue