[7.x] [canvas] TS Asset Manager + Stories (#31341) (#35097)

Backports the following commits to 7.x:
 - [canvas] TS Asset Manager + Stories  (#31341)
This commit is contained in:
Clint Andrew Hall 2019-04-15 16:09:16 -05:00 committed by GitHub
parent a304b2918a
commit c1a11d0bb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1253 additions and 362 deletions

View file

@ -42,6 +42,7 @@
"@storybook/react": "^5.0.5",
"@storybook/theming": "^5.0.5",
"@types/angular": "1.6.50",
"@types/base64-js": "^1.2.5",
"@types/cheerio": "^0.22.10",
"@types/chroma-js": "^1.4.1",
"@types/color": "^3.0.0",
@ -51,6 +52,7 @@
"@types/d3-time-format": "^2.1.0",
"@types/d3-time": "^1.0.7",
"@types/elasticsearch": "^5.0.30",
"@types/file-saver": "^2.0.0",
"@types/graphql": "^0.13.1",
"@types/history": "^4.6.2",
"@types/jest": "^24.0.9",
@ -58,6 +60,7 @@
"@types/json-stable-stringify": "^1.0.32",
"@types/jsonwebtoken": "^7.2.7",
"@types/lodash": "^3.10.1",
"@types/mime": "^2.0.1",
"@types/mocha": "^5.2.6",
"@types/object-hash": "^1.2.0",
"@types/pngjs": "^3.3.1",

View file

@ -0,0 +1,88 @@
/*
* 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 { isValidDataUrl, parseDataUrl } from '../dataurl';
const BASE64_TEXT = 'data:text/plain;charset=utf-8;base64,VGhpcyBpcyBhIHRlc3Q=';
const BASE64_SVG =
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=';
const BASE64_PIXEL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk8PxfDwADYgHJvQ16TAAAAABJRU5ErkJggg==';
const RAW_TEXT = 'data:text/plain;charset=utf-8,This%20is%20a%20test';
const RAW_SVG =
'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%2F%3E';
const RAW_PIXEL =
'data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%01%00%00%00%01%08%06%00%00%00%1F%15%C4%89%00%00%00%0DIDATx%DAcd%F0%FC_%0F%00%03b%01%C9%BD%0DzL%00%00%00%00IEND%AEB%60%82';
describe('dataurl', () => {
describe('isValidDataUrl', () => {
test('invalid data url', () => {
expect(isValidDataUrl('somestring')).toBe(false);
});
test('valid data urls', () => {
expect(isValidDataUrl(BASE64_TEXT)).toBe(true);
expect(isValidDataUrl(BASE64_SVG)).toBe(true);
expect(isValidDataUrl(BASE64_PIXEL)).toBe(true);
expect(isValidDataUrl(RAW_TEXT)).toBe(true);
expect(isValidDataUrl(RAW_SVG)).toBe(true);
expect(isValidDataUrl(RAW_PIXEL)).toBe(true);
});
});
describe('dataurl.parseDataUrl', () => {
test('invalid data url', () => {
expect(parseDataUrl('somestring')).toBeNull();
});
test('text data urls', () => {
expect(parseDataUrl(BASE64_TEXT)).toEqual({
charset: 'utf-8',
data: null,
encoding: 'base64',
extension: 'txt',
isImage: false,
mimetype: 'text/plain',
});
expect(parseDataUrl(RAW_TEXT)).toEqual({
charset: 'utf-8',
data: null,
encoding: undefined,
extension: 'txt',
isImage: false,
mimetype: 'text/plain',
});
});
test('png data urls', () => {
expect(parseDataUrl(RAW_PIXEL)).toBeNull();
expect(parseDataUrl(BASE64_PIXEL)).toEqual({
charset: undefined,
data: null,
encoding: 'base64',
extension: 'png',
isImage: true,
mimetype: 'image/png',
});
});
test('svg data urls', () => {
expect(parseDataUrl(RAW_SVG)).toEqual({
charset: undefined,
data: null,
encoding: undefined,
extension: 'svg',
isImage: true,
mimetype: 'image/svg+xml',
});
expect(parseDataUrl(BASE64_SVG)).toEqual({
charset: undefined,
data: null,
encoding: 'base64',
extension: 'svg',
isImage: true,
mimetype: 'image/svg+xml',
});
});
});
});

View file

@ -0,0 +1,27 @@
/*
* 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 { hexToRgb } from '../hex_to_rgb';
describe('hexToRgb', () => {
test('invalid hex', () => {
expect(hexToRgb('hexadecimal')).toBeNull();
expect(hexToRgb('#00')).toBeNull();
expect(hexToRgb('#00000')).toBeNull();
});
test('shorthand', () => {
expect(hexToRgb('#000')).toEqual([0, 0, 0]);
expect(hexToRgb('#FFF')).toEqual([255, 255, 255]);
expect(hexToRgb('#fff')).toEqual([255, 255, 255]);
expect(hexToRgb('#fFf')).toEqual([255, 255, 255]);
});
test('longhand', () => {
expect(hexToRgb('#000000')).toEqual([0, 0, 0]);
expect(hexToRgb('#ffffff')).toEqual([255, 255, 255]);
expect(hexToRgb('#fffFFF')).toEqual([255, 255, 255]);
expect(hexToRgb('#FFFFFF')).toEqual([255, 255, 255]);
});
});

View file

@ -5,21 +5,23 @@
*/
import { fromByteArray } from 'base64-js';
// @ts-ignore @types/mime doesn't resolve mime/lite for some reason.
import mime from 'mime/lite';
const dataurlRegex = /^data:([a-z]+\/[a-z0-9-+.]+)(;[a-z-]+=[a-z0-9-]+)?(;([a-z0-9]+))?,/;
export const imageTypes = ['image/svg+xml', 'image/jpeg', 'image/png', 'image/gif'];
export function parseDataUrl(str, withData = false) {
export function parseDataUrl(str: string, withData = false) {
if (typeof str !== 'string') {
return;
return null;
}
const matches = str.match(dataurlRegex);
if (!matches) {
return;
return null;
}
const [, mimetype, charset, , encoding] = matches;
@ -27,7 +29,7 @@ export function parseDataUrl(str, withData = false) {
// all types except for svg need to be base64 encoded
const imageTypeIndex = imageTypes.indexOf(matches[1]);
if (imageTypeIndex > 0 && encoding !== 'base64') {
return;
return null;
}
return {
@ -40,14 +42,14 @@ export function parseDataUrl(str, withData = false) {
};
}
export function isValidDataUrl(str) {
export function isValidDataUrl(str: string) {
return dataurlRegex.test(str);
}
export function encode(data, type = 'text/plain') {
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) => {
return new Promise<string | ArrayBuffer | null>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = err => reject(err);

View file

@ -4,18 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const hexToRgb = hex => {
export const hexToRgb = (hex: string) => {
const shorthandHexColor = /^#?([a-f\d]{1})([a-f\d]{1})([a-f\d]{1})$/i;
const hexColor = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
const shorthandMatches = shorthandHexColor.exec(hex);
if (shorthandMatches) {
return shorthandMatches.slice(1, 4).map(hex => parseInt(hex + hex, 16));
return shorthandMatches.slice(1, 4).map(mappedHex => parseInt(mappedHex + mappedHex, 16));
}
const hexMatches = hexColor.exec(hex);
if (hexMatches) {
return hexMatches.slice(1, 4).map(hex => parseInt(hex, 16));
return hexMatches.slice(1, 4).map(slicedHex => parseInt(slicedHex, 16));
}
return null;

View file

@ -0,0 +1,445 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/Asset airplane 1`] = `
<div
style={
Object {
"width": "215px",
}
}
>
<div
className="euiFlexItem"
>
<div
className="euiPanel euiPanel--paddingSmall canvasAsset"
>
<div
className="canvasAsset__thumb canvasCheckered"
>
<figure
className="euiImage canvasAsset__img"
style={
Object {
"backgroundImage": "url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=)",
}
}
>
<img
alt="Asset thumbnail"
className="euiImage__img"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4="
/>
</figure>
</div>
<div
className="euiSpacer euiSpacer--s"
/>
<div
className="euiText euiText--extraSmall eui-textBreakAll"
>
<p
className="eui-textBreakAll"
>
<strong>
airplane
</strong>
<br />
<span
className="euiTextColor euiTextColor--subdued"
>
<small>
(
1
kb)
</small>
</span>
</p>
</div>
<div
className="euiSpacer euiSpacer--s"
/>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-create-image"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-describedby="generated-id"
aria-label="Create image element"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.5 11V5H11V3.5H5V5H3.5v6H5v1.5h6V11h1.5zm1 0H15v4h-4v-1.5H5V15H1v-4h1.5V5H1V1h4v1.5h6V1h4v4h-1.5v6zM4 4V2H2v2h2zm8 0h2V2h-2v2zM2 14h2v-2H2v2zm10 0h2v-2h-2v2z"
/>
</svg>
</button>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-download"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="canvasDownload"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<button
aria-label="Download"
className="euiButtonIcon euiButtonIcon--primary"
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 11.692V3.556C7 3.249 7.224 3 7.5 3s.5.249.5.556v8.136l4.096-4.096a.5.5 0 0 1 .707.707l-4.242 4.243a1.494 1.494 0 0 1-.925.433.454.454 0 0 1-.272 0 1.494 1.494 0 0 1-.925-.433L2.197 8.303a.5.5 0 1 1 .707-.707L7 11.692z"
/>
</svg>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="canvasClipboard"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<button
aria-label="Copy id to clipboard"
className="euiButtonIcon euiButtonIcon--primary"
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 2.729V2a1 1 0 0 1 1-1h2v1H1v12h4v1H1a1 1 0 0 1-1-1V2.729zM12 5V2a1 1 0 0 0-1-1H9v1h2v3h1zm-1 1h2v9H6V6h5V5H6a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2v1z"
/>
<path
d="M7 10h5V9H7zM7 8h5V7H7zM7 12h5v-1H7zM7 14h5v-1H7zM9 2V1a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v1h1V1h4v1h1zM3 3h6V2H3z"
/>
</svg>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-describedby="generated-id"
aria-label="Delete"
className="euiButtonIcon euiButtonIcon--danger"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<path
d="M11 3h5v1H0V3h5V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2zm-7.056 8H7v1H4.1l.392 2.519c.042.269.254.458.493.458h6.03c.239 0 .451-.189.493-.458l1.498-9.576H14l-1.504 9.73c-.116.747-.74 1.304-1.481 1.304h-6.03c-.741 0-1.365-.557-1.481-1.304l-1.511-9.73H9V5.95H3.157L3.476 8H8v1H3.632l.312 2zM6 3h4V1H6v2z"
id="trash-a"
/>
</defs>
<use
xlinkHref="#trash-a"
/>
</svg>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots components/Asset marker 1`] = `
<div
style={
Object {
"width": "215px",
}
}
>
<div
className="euiFlexItem"
>
<div
className="euiPanel euiPanel--paddingSmall canvasAsset"
>
<div
className="canvasAsset__thumb canvasCheckered"
>
<figure
className="euiImage canvasAsset__img"
style={
Object {
"backgroundImage": "url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=)",
}
}
>
<img
alt="Asset thumbnail"
className="euiImage__img"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4="
/>
</figure>
</div>
<div
className="euiSpacer euiSpacer--s"
/>
<div
className="euiText euiText--extraSmall eui-textBreakAll"
>
<p
className="eui-textBreakAll"
>
<strong>
marker
</strong>
<br />
<span
className="euiTextColor euiTextColor--subdued"
>
<small>
(
1
kb)
</small>
</span>
</p>
</div>
<div
className="euiSpacer euiSpacer--s"
/>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-create-image"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-describedby="generated-id"
aria-label="Create image element"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.5 11V5H11V3.5H5V5H3.5v6H5v1.5h6V11h1.5zm1 0H15v4h-4v-1.5H5V15H1v-4h1.5V5H1V1h4v1.5h6V1h4v4h-1.5v6zM4 4V2H2v2h2zm8 0h2V2h-2v2zM2 14h2v-2H2v2zm10 0h2v-2h-2v2z"
/>
</svg>
</button>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-download"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="canvasDownload"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<button
aria-label="Download"
className="euiButtonIcon euiButtonIcon--primary"
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 11.692V3.556C7 3.249 7.224 3 7.5 3s.5.249.5.556v8.136l4.096-4.096a.5.5 0 0 1 .707.707l-4.242 4.243a1.494 1.494 0 0 1-.925.433.454.454 0 0 1-.272 0 1.494 1.494 0 0 1-.925-.433L2.197 8.303a.5.5 0 1 1 .707-.707L7 11.692z"
/>
</svg>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="canvasClipboard"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<button
aria-label="Copy id to clipboard"
className="euiButtonIcon euiButtonIcon--primary"
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 2.729V2a1 1 0 0 1 1-1h2v1H1v12h4v1H1a1 1 0 0 1-1-1V2.729zM12 5V2a1 1 0 0 0-1-1H9v1h2v3h1zm-1 1h2v9H6V6h5V5H6a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2v1z"
/>
<path
d="M7 10h5V9H7zM7 8h5V7H7zM7 12h5v-1H7zM7 14h5v-1H7zM9 2V1a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v1h1V1h4v1h1zM3 3h6V2H3z"
/>
</svg>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-describedby="generated-id"
aria-label="Delete"
className="euiButtonIcon euiButtonIcon--danger"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<path
d="M11 3h5v1H0V3h5V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2zm-7.056 8H7v1H4.1l.392 2.519c.042.269.254.458.493.458h6.03c.239 0 .451-.189.493-.458l1.498-9.576H14l-1.504 9.73c-.116.747-.74 1.304-1.481 1.304h-6.03c-.741 0-1.365-.557-1.481-1.304l-1.511-9.73H9V5.95H3.157L3.476 8H8v1H3.632l.312 2zM6 3h4V1H6v2z"
id="trash-a"
/>
</defs>
<use
xlinkHref="#trash-a"
/>
</svg>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/AssetManager no assets 1`] = `
<button
className="euiButtonEmpty euiButtonEmpty--primary"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Manage assets
</span>
</span>
</button>
`;
exports[`Storyshots components/AssetManager two assets 1`] = `
<button
className="euiButtonEmpty euiButtonEmpty--primary"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Manage assets
</span>
</span>
</button>
`;

View file

@ -0,0 +1,45 @@
/*
* 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 { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { Asset, AssetType } from '../asset';
const AIRPLANE: AssetType = {
'@created': '2018-10-13T16:44:44.648Z',
id: 'airplane',
type: 'dataurl',
value:
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=',
};
const MARKER: AssetType = {
'@created': '2018-10-13T16:44:44.648Z',
id: 'marker',
type: 'dataurl',
value:
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=',
};
storiesOf('components/Asset', module)
.addDecorator(story => <div style={{ width: '215px' }}>{story()}</div>)
.add('airplane', () => (
<Asset
asset={AIRPLANE}
onCreate={action('onCreate')}
onCopy={action('onCopy')}
onDelete={action('onDelete')}
/>
))
.add('marker', () => (
<Asset
asset={MARKER}
onCreate={action('onCreate')}
onCopy={action('onCopy')}
onDelete={action('onDelete')}
/>
));

View file

@ -0,0 +1,47 @@
/*
* 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 { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { AssetType } from '../asset';
import { AssetManager } from '../asset_manager';
const AIRPLANE: AssetType = {
'@created': '2018-10-13T16:44:44.648Z',
id: 'airplane',
type: 'dataurl',
value:
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=',
};
const MARKER: AssetType = {
'@created': '2018-10-13T16:44:44.648Z',
id: 'marker',
type: 'dataurl',
value:
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=',
};
storiesOf('components/AssetManager', module)
.add('no assets', () => (
// @ts-ignore @types/react has not been updated to support defaultProps yet.
<AssetManager
onAddImageElement={action('onAddImageElement')}
onAssetAdd={action('onAssetAdd')}
onAssetCopy={action('onAssetCopy')}
onAssetDelete={action('onAssetDelete')}
/>
))
.add('two assets', () => (
<AssetManager
assetValues={[AIRPLANE, MARKER]}
onAddImageElement={action('onAddImageElement')}
onAssetAdd={action('onAssetAdd')}
onAssetCopy={action('onAssetCopy')}
onAssetDelete={action('onAssetDelete')}
/>
));

View file

@ -0,0 +1,131 @@
/*
* 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 {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
// @ts-ignore (elastic/eui#1262) EuiImage is not exported yet
EuiImage,
EuiPanel,
EuiSpacer,
EuiText,
EuiTextColor,
EuiToolTip,
} from '@elastic/eui';
import React, { FunctionComponent } from 'react';
import { Clipboard } from '../clipboard';
import { Download } from '../download';
type ValidTypes = 'dataurl';
export interface AssetType {
'@created': string;
id: string;
type: ValidTypes;
value: string;
}
interface Props {
/** The asset to be rendered */
asset: AssetType;
/** The function to execute when the user clicks 'Create' */
onCreate: (asset: AssetType) => void;
/** The function to execute when the user clicks 'Copy' */
onCopy: (asset: AssetType) => void;
/** The function to execute when the user clicks 'Delete' */
onDelete: (asset: AssetType) => void;
}
export const Asset: FunctionComponent<Props> = props => {
const { asset, onCreate, onCopy, onDelete } = props;
const createImage = (
<EuiFlexItem className="asset-create-image" grow={false}>
<EuiToolTip content="Create image element">
<EuiButtonIcon
iconType="vector"
aria-label="Create image element"
onClick={() => onCreate(asset)}
/>
</EuiToolTip>
</EuiFlexItem>
);
const downloadAsset = (
<EuiFlexItem className="asset-download" grow={false}>
<EuiToolTip content="Download">
<Download fileName={asset.id} content={asset.value}>
<EuiButtonIcon iconType="sortDown" aria-label="Download" />
</Download>
</EuiToolTip>
</EuiFlexItem>
);
const copyAsset = (
<EuiFlexItem grow={false}>
<EuiToolTip content="Copy id to clipboard">
<Clipboard content={asset.id} onCopy={(result: boolean) => result && onCopy(asset)}>
<EuiButtonIcon iconType="copyClipboard" aria-label="Copy id to clipboard" />
</Clipboard>
</EuiToolTip>
</EuiFlexItem>
);
const deleteAsset = (
<EuiFlexItem grow={false}>
<EuiToolTip content="Delete">
<EuiButtonIcon
color="danger"
iconType="trash"
aria-label="Delete"
onClick={() => onDelete(asset)}
/>
</EuiToolTip>
</EuiFlexItem>
);
const thumbnail = (
<div className="canvasAsset__thumb canvasCheckered">
<EuiImage
className="canvasAsset__img"
size="original"
url={props.asset.value}
fullScreenIconColor="dark"
alt="Asset thumbnail"
style={{ backgroundImage: `url(${props.asset.value})` }}
/>
</div>
);
const assetLabel = (
<EuiText size="xs" className="eui-textBreakAll">
<p className="eui-textBreakAll">
<strong>{asset.id}</strong>
<br />
<EuiTextColor color="subdued">
<small>({Math.round(asset.value.length / 1024)} kb)</small>
</EuiTextColor>
</p>
</EuiText>
);
return (
<EuiFlexItem key={props.asset.id}>
<EuiPanel className="canvasAsset" paddingSize="s">
{thumbnail}
<EuiSpacer size="s" />
{assetLabel}
<EuiSpacer size="s" />
<EuiFlexGroup alignItems="baseline" justifyContent="center" responsive={false}>
{createImage}
{downloadAsset}
{copyAsset}
{deleteAsset}
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
);
};

View file

@ -1,258 +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 React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonIcon,
EuiButtonEmpty,
EuiButton,
EuiOverlayMask,
EuiModal,
EuiModalHeader,
EuiModalBody,
EuiText,
EuiImage,
EuiPanel,
EuiModalFooter,
EuiModalHeaderTitle,
EuiFlexGrid,
EuiProgress,
EuiSpacer,
EuiTextColor,
EuiToolTip,
EuiFilePicker,
EuiEmptyPrompt,
} from '@elastic/eui';
import { ConfirmModal } from '../confirm_modal';
import { Clipboard } from '../clipboard';
import { Download } from '../download';
import { Loading } from '../loading';
import { ASSET_MAX_SIZE } from '../../../common/lib/constants';
export class AssetManager extends React.PureComponent {
static propTypes = {
assetValues: PropTypes.array,
addImageElement: PropTypes.func,
removeAsset: PropTypes.func.isRequired,
copyAsset: PropTypes.func.isRequired,
onAssetAdd: PropTypes.func.isRequired,
};
state = {
deleteId: null,
isModalVisible: false,
loading: false,
};
_isMounted = true;
showModal = () => this.setState({ isModalVisible: true });
closeModal = () => this.setState({ isModalVisible: false });
doDelete = () => {
this.resetDelete();
this.props.removeAsset(this.state.deleteId);
};
handleFileUpload = files => {
this.setState({ loading: true });
Promise.all(Array.from(files).map(file => this.props.onAssetAdd(file))).finally(() => {
this._isMounted && this.setState({ loading: false });
});
};
addElement = assetId => {
this.props.addImageElement(assetId);
};
resetDelete = () => this.setState({ deleteId: null });
renderAsset = asset => (
<EuiFlexItem key={asset.id}>
<EuiPanel className="canvasAssetManager__asset" paddingSize="s">
<div className="canvasAssetManager__thumb canvasCheckered">
<EuiImage
className="canvasAssetManager__img"
size="original"
url={asset.value}
fullScreenIconColor="dark"
alt="Asset thumbnail"
style={{ backgroundImage: `url(${asset.value})` }}
/>
</div>
<EuiSpacer size="s" />
<EuiText size="xs" className="eui-textBreakAll">
<p className="eui-textBreakAll">
<strong>{asset.id}</strong>
<br />
<EuiTextColor color="subdued">
<small>({Math.round(asset.value.length / 1024)} kb)</small>
</EuiTextColor>
</p>
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup alignItems="baseline" justifyContent="center" responsive={false}>
<EuiFlexItem className="asset-create-image" grow={false}>
<EuiToolTip content="Create image element">
<EuiButtonIcon
iconType="vector"
aria-label="Create image element"
onClick={() => {
this.addElement(asset.id);
this.closeModal();
}}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem className="asset-download" grow={false}>
<EuiToolTip content="Download">
<Download fileName={asset.id} content={asset.value}>
<EuiButtonIcon iconType="sortDown" aria-label="Download" />
</Download>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content="Copy id to clipboard">
<Clipboard
content={asset.id}
onCopy={result => result && this.props.copyAsset(asset.id)}
>
<EuiButtonIcon iconType="copyClipboard" aria-label="Copy id to clipboard" />
</Clipboard>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content="Delete">
<EuiButtonIcon
color="danger"
iconType="trash"
aria-label="Delete"
onClick={() => this.setState({ deleteId: asset.id })}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
);
render() {
const { isModalVisible, loading } = this.state;
const { assetValues } = this.props;
const assetsTotal = Math.round(
assetValues.reduce((total, { value }) => total + value.length, 0) / 1024
);
const percentageUsed = Math.round((assetsTotal / ASSET_MAX_SIZE) * 100);
const emptyAssets = (
<EuiPanel className="canvasAssetManager__emptyPanel">
<EuiEmptyPrompt
iconType="importAction"
title={<h2>No available assets</h2>}
titleSize="s"
body={
<Fragment>
<p>Upload your assets above to get started</p>
</Fragment>
}
/>
</EuiPanel>
);
const assetModal = isModalVisible ? (
<EuiOverlayMask>
<EuiModal
onClose={this.closeModal}
className="canvasAssetManager canvasModal--fixedSize"
maxWidth="1000px"
>
<EuiModalHeader className="canvasAssetManager__modalHeader">
<EuiModalHeaderTitle className="canvasAssetManager__modalHeaderTitle">
Manage workpad assets
</EuiModalHeaderTitle>
<EuiFlexGroup className="canvasAssetManager__fileUploadWrapper">
<EuiFlexItem grow={false}>
{loading ? (
<Loading animated text="Uploading images" />
) : (
<EuiFilePicker
initialPromptText="Select or drag and drop images"
compressed
multiple
onChange={this.handleFileUpload}
accept="image/*"
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalHeader>
<EuiModalBody>
<EuiText size="s" color="subdued">
<p>
Below are the image assets that you added to this workpad. To reclaim space, delete
assets that you no longer need. Unfortunately, any assets that are actually in use
cannot be determined at this time.
</p>
</EuiText>
<EuiSpacer />
{assetValues.length ? (
<EuiFlexGrid responsive={false} columns={4}>
{assetValues.map(this.renderAsset)}
</EuiFlexGrid>
) : (
emptyAssets
)}
</EuiModalBody>
<EuiModalFooter className="canvasAssetManager__modalFooter">
<EuiFlexGroup className="canvasAssetManager__meterWrapper" responsive={false}>
<EuiFlexItem>
<EuiProgress
value={assetsTotal}
max={ASSET_MAX_SIZE}
color={percentageUsed < 90 ? 'secondary' : 'danger'}
size="s"
aria-labelledby="CanvasAssetManagerLabel"
/>
</EuiFlexItem>
<EuiFlexItem grow={false} className="eui-textNoWrap">
<EuiText id="CanvasAssetManagerLabel">{percentageUsed}% space used</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiButton size="s" onClick={this.closeModal}>
Close
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
) : null;
return (
<Fragment>
<EuiButtonEmpty onClick={this.showModal}>Manage assets</EuiButtonEmpty>
{assetModal}
<ConfirmModal
isOpen={this.state.deleteId != null}
title="Remove Asset"
message="Are you sure you want to remove this asset?"
confirmButtonText="Remove"
onConfirm={this.doDelete}
onCancel={this.resetDelete}
/>
</Fragment>
);
}
}

View file

@ -29,25 +29,27 @@
padding-right: $euiSize;
}
// ASSETS LIST
.canvasAssetManager__asset {
text-align: center;
overflow: hidden; // hides image from outer panel boundaries
.canvasAssetManager__modalFooter {
justify-content: space-between;
}
.canvasAssetManager__emptyPanel {
max-width: 400px;
margin: 0 auto;
}
}
.canvasAssetManager__thumb {
.canvasAsset {
text-align: center;
overflow: hidden; // hides image from outer panel boundaries
.canvasAsset__thumb {
margin: -$euiSizeS;
margin-bottom: 0;
font-size: 0; // eliminates any extra space around img
}
.canvasAssetManager__img {
.canvasAsset__img {
background-repeat: no-repeat;
background-position: center;
background-size: contain;
@ -59,8 +61,4 @@
opacity: 0; // only show the background image (which will properly keep proportions)
}
}
.canvasAssetManager__modalFooter {
justify-content: space-between;
}
}

View file

@ -0,0 +1,114 @@
/*
* 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 {
EuiButtonEmpty,
// @ts-ignore (elastic/eui#1557) EuiFilePicker is not exported yet
EuiFilePicker,
// @ts-ignore (elastic/eui#1557) EuiImage is not exported yet
EuiImage,
} from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { Fragment, PureComponent } from 'react';
import { ConfirmModal } from '../confirm_modal';
import { AssetType } from './asset';
import { AssetModal } from './asset_modal';
interface Props {
/** A list of assets, if available */
assetValues: AssetType[];
/** Function to invoke when an asset is selected to be added as an element to the workpad */
onAddImageElement: (id: string) => void;
/** Function to invoke when an asset is deleted */
onAssetDelete: (id: string | null) => void;
/** Function to invoke when an asset is copied */
onAssetCopy: () => void;
/** Function to invoke when an asset is added */
onAssetAdd: (asset: File) => void;
}
interface State {
/** The id of the asset to delete, if applicable. Is set if the viewer clicks the delete icon */
deleteId: string | null;
/** Determines if the modal is currently visible */
isModalVisible: boolean;
/** Indicates if the modal is currently loading */
isLoading: boolean;
}
export class AssetManager extends PureComponent<Props, State> {
public static propTypes = {
assetValues: PropTypes.array,
onAddImageElement: PropTypes.func.isRequired,
onAssetAdd: PropTypes.func.isRequired,
onAssetCopy: PropTypes.func.isRequired,
onAssetDelete: PropTypes.func.isRequired,
};
public static defaultProps = {
assetValues: [],
};
public state = {
deleteId: null,
isLoading: false,
isModalVisible: false,
};
public render() {
const { isModalVisible, isLoading } = this.state;
const { assetValues, onAssetCopy, onAddImageElement } = this.props;
const assetModal = (
<AssetModal
assetValues={assetValues}
isLoading={isLoading}
onAssetCopy={onAssetCopy}
onAssetCreate={(createdAsset: AssetType) => {
onAddImageElement(createdAsset.id);
this.setState({ isModalVisible: false });
}}
onAssetDelete={(asset: AssetType) => this.setState({ deleteId: asset.id })}
onClose={() => this.setState({ isModalVisible: false })}
onFileUpload={this.handleFileUpload}
/>
);
const confirmModal = (
<ConfirmModal
isOpen={this.state.deleteId !== null}
title="Remove Asset"
message="Are you sure you want to remove this asset?"
confirmButtonText="Remove"
onConfirm={this.doDelete}
onCancel={this.resetDelete}
/>
);
return (
<Fragment>
<EuiButtonEmpty onClick={this.showModal}>Manage assets</EuiButtonEmpty>
{isModalVisible ? assetModal : null}
{confirmModal}
</Fragment>
);
}
private showModal = () => this.setState({ isModalVisible: true });
private resetDelete = () => this.setState({ deleteId: null });
private doDelete = () => {
this.resetDelete();
this.props.onAssetDelete(this.state.deleteId);
};
private handleFileUpload = (files: FileList) => {
this.setState({ isLoading: true });
Promise.all(Array.from(files).map(file => this.props.onAssetAdd(file))).finally(() => {
this.setState({ isLoading: false });
});
};
}

View file

@ -0,0 +1,159 @@
/*
* 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 {
EuiButton,
EuiEmptyPrompt,
// @ts-ignore (elastic/eui#1557) EuiFilePicker is not exported yet
EuiFilePicker,
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiPanel,
EuiProgress,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
// @ts-ignore
import { ASSET_MAX_SIZE } from '../../../common/lib/constants';
import { Loading } from '../loading';
import { Asset, AssetType } from './asset';
interface Props {
/** The assets to display within the modal */
assetValues: AssetType[];
/** Indicates if assets are being loaded */
isLoading: boolean;
/** Function to invoke when the modal is closed */
onClose: () => void;
/** Function to invoke when a file is uploaded */
onFileUpload: (assets: FileList) => void;
/** Function to invoke when an asset is copied */
onAssetCopy: (asset: AssetType) => void;
/** Function to invoke when an asset is created */
onAssetCreate: (asset: AssetType) => void;
/** Function to invoke when an asset is deleted */
onAssetDelete: (asset: AssetType) => void;
}
export const AssetModal: FunctionComponent<Props> = props => {
const {
assetValues,
isLoading,
onAssetCopy,
onAssetCreate,
onAssetDelete,
onClose,
onFileUpload,
} = props;
const assetsTotal = Math.round(
assetValues.reduce((total, { value }) => total + value.length, 0) / 1024
);
const percentageUsed = Math.round((assetsTotal / ASSET_MAX_SIZE) * 100);
const emptyAssets = (
<EuiPanel className="canvasAssetManager__emptyPanel">
<EuiEmptyPrompt
iconType="importAction"
title={<h2>Import your assets to get started</h2>}
titleSize="xs"
/>
</EuiPanel>
);
return (
<EuiOverlayMask>
<EuiModal
onClose={onClose}
className="canvasAssetManager canvasModal--fixedSize"
maxWidth="1000px"
>
<EuiModalHeader className="canvasAssetManager__modalHeader">
<EuiModalHeaderTitle className="canvasAssetManager__modalHeaderTitle">
Manage workpad assets
</EuiModalHeaderTitle>
<EuiFlexGroup className="canvasAssetManager__fileUploadWrapper">
<EuiFlexItem grow={false}>
{isLoading ? (
<Loading animated text="Uploading images" />
) : (
<EuiFilePicker
initialPromptText="Select or drag and drop images"
compressed
multiple
onChange={onFileUpload}
accept="image/*"
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalHeader>
<EuiModalBody>
<EuiText size="s" color="subdued">
<p>
Below are the image assets in this workpad. Any assets that are currently in use
cannot be determined at this time. To reclaim space, delete assets.
</p>
</EuiText>
<EuiSpacer />
{assetValues.length ? (
<EuiFlexGrid columns={4}>
{assetValues.map(asset => (
<Asset
asset={asset}
key={asset.id}
onCopy={onAssetCopy}
onCreate={onAssetCreate}
onDelete={onAssetDelete}
/>
))}
</EuiFlexGrid>
) : (
emptyAssets
)}
</EuiModalBody>
<EuiModalFooter className="canvasAssetManager__modalFooter">
<EuiFlexGroup className="canvasAssetManager__meterWrapper" responsive={false}>
<EuiFlexItem>
<EuiProgress
value={assetsTotal}
max={ASSET_MAX_SIZE}
color={percentageUsed < 90 ? 'secondary' : 'danger'}
size="s"
aria-labelledby="CanvasAssetManagerLabel"
/>
</EuiFlexItem>
<EuiFlexItem grow={false} className="eui-textNoWrap">
<EuiText id="CanvasAssetManagerLabel">{percentageUsed}% space used</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiButton size="s" onClick={onClose}>
Close
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
};
AssetModal.propTypes = {
assetValues: PropTypes.array,
isLoading: PropTypes.bool,
onClose: PropTypes.func.isRequired,
onFileUpload: PropTypes.func.isRequired,
onAssetCopy: PropTypes.func.isRequired,
onAssetCreate: PropTypes.func.isRequired,
onAssetDelete: PropTypes.func.isRequired,
};

View file

@ -26,7 +26,7 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
addImageElement: pageId => assetId => {
onAddImageElement: pageId => assetId => {
const imageElement = elementsRegistry.get('image');
const elementAST = fromExpression(imageElement.expression);
const selector = ['chain', '0', 'arguments', 'dataurl'];
@ -56,7 +56,7 @@ const mapDispatchToProps = dispatch => ({
// then return the id, so the caller knows the id that will be created
return assetId;
},
removeAsset: assetId => dispatch(removeAsset(assetId)),
onAssetDelete: assetId => dispatch(removeAsset(assetId)),
});
const mergeProps = (stateProps, dispatchProps, ownProps) => {
@ -67,9 +67,9 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
return {
...ownProps,
...dispatchProps,
onAddImageElement: dispatchProps.onAddImageElement(stateProps.selectedPage),
selectedPage,
assetValues,
addImageElement: dispatchProps.addImageElement(stateProps.selectedPage),
onAssetAdd: file => {
const [type, subtype] = get(file, 'type', '').split('/');
if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) {
@ -94,5 +94,5 @@ export const AssetManager = compose(
mapDispatchToProps,
mergeProps
),
withProps({ copyAsset: assetId => notify.success(`Copied '${assetId}' to clipboard`) })
withProps({ onAssetCopy: asset => notify.success(`Copied '${asset.id}' to clipboard`) })
)(Component);

View file

@ -4,33 +4,40 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import copy from 'copy-to-clipboard';
import PropTypes from 'prop-types';
import React, { MouseEvent, KeyboardEvent, ReactElement } from 'react';
export class Clipboard extends React.PureComponent {
static propTypes = {
interface Props {
children: ReactElement<any>;
content: string | number;
onCopy: (result: boolean) => void;
}
export class Clipboard extends React.PureComponent<Props> {
public static propTypes = {
children: PropTypes.element.isRequired,
content: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
onCopy: PropTypes.func,
};
onClick = ev => {
const { content, onCopy } = this.props;
ev.preventDefault();
const result = copy(content, { debug: true });
if (typeof onCopy === 'function') {
onCopy(result);
}
};
render() {
public render() {
return (
<div className="canvasClipboard" onClick={this.onClick}>
<div
className="canvasClipboard"
onClick={this.onClick}
onKeyPress={this.onClick}
role="button"
tabIndex={0}
>
{this.props.children}
</div>
);
}
private onClick = (ev: MouseEvent<HTMLDivElement> | KeyboardEvent) => {
const { content, onCopy } = this.props;
ev.preventDefault();
onCopy(copy(content.toString(), { debug: true }));
};
}

View file

@ -5,11 +5,22 @@
*/
/* eslint-disable react/forbid-elements */
import React from 'react';
import PropTypes from 'prop-types';
import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
export const ConfirmModal = props => {
interface Props {
isOpen: boolean;
title?: string;
message: string;
onConfirm: () => void;
onCancel: () => void;
cancelButtonText?: string;
confirmButtonText?: string;
className?: string;
}
export const ConfirmModal: FunctionComponent<Props> = props => {
const {
isOpen,
title,
@ -22,14 +33,6 @@ export const ConfirmModal = props => {
...rest
} = props;
const confirm = ev => {
onConfirm && onConfirm(ev);
};
const cancel = ev => {
onCancel && onCancel(ev);
};
// render nothing if this component isn't open
if (!isOpen) {
return null;
@ -41,8 +44,8 @@ export const ConfirmModal = props => {
{...rest}
className={`canvasConfirmModal ${className || ''}`}
title={title}
onCancel={cancel}
onConfirm={confirm}
onCancel={onCancel}
onConfirm={onConfirm}
confirmButtonText={confirmButtonText}
cancelButtonText={cancelButtonText}
defaultFocusedButton="confirm"

View file

@ -4,19 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import expect from '@kbn/expect';
import { render } from 'enzyme';
import { Download } from '../';
import React from 'react';
import { Download } from '..';
describe('<Download />', () => {
it('has canvasDownload class', () => {
test('has canvasDownload class', () => {
const wrapper = render(
<Download fileName="hello" content="world">
<button>Download it</button>
</Download>
);
expect(wrapper.hasClass('canvasDownload')).to.be.ok;
expect(wrapper.hasClass('canvasDownload')).toBeTruthy();
});
});

View file

@ -1,36 +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 React from 'react';
import PropTypes from 'prop-types';
import fileSaver from 'file-saver';
import { toByteArray } from 'base64-js';
import { parseDataUrl } from '../../../common/lib/dataurl';
export class Download extends React.PureComponent {
static propTypes = {
children: PropTypes.element.isRequired,
fileName: PropTypes.string,
content: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
onCopy: PropTypes.func,
};
onClick = () => {
const { fileName, content } = this.props;
const asset = parseDataUrl(content, true);
const assetBlob = new Blob([toByteArray(asset.data)], { type: asset.mimetype });
const ext = asset.extension ? `.${asset.extension}` : '';
fileSaver.saveAs(assetBlob, `canvas-${fileName}${ext}`);
};
render() {
return (
<div className="canvasDownload" onClick={this.onClick}>
{this.props.children}
</div>
);
}
}

View file

@ -0,0 +1,50 @@
/*
* 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 { toByteArray } from 'base64-js';
import fileSaver from 'file-saver';
import PropTypes from 'prop-types';
import React, { ReactElement } from 'react';
import { parseDataUrl } from '../../../common/lib/dataurl';
interface Props {
children: ReactElement<any>;
fileName: string;
content: string;
}
export class Download extends React.PureComponent<Props> {
public static propTypes = {
children: PropTypes.element.isRequired,
fileName: PropTypes.string.isRequired,
content: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
};
public onClick = () => {
const { fileName, content } = this.props;
const asset = parseDataUrl(content, true);
if (asset && asset.data) {
const assetBlob = new Blob([toByteArray(asset.data)], { type: asset.mimetype });
const ext = asset.extension ? `.${asset.extension}` : '';
fileSaver.saveAs(assetBlob, `canvas-${fileName}${ext}`);
}
};
public render() {
return (
<div
className="canvasDownload"
onClick={this.onClick}
onKeyPress={this.onClick}
tabIndex={0}
role="button"
>
{this.props.children}
</div>
);
}
}

View file

@ -7,7 +7,7 @@
// @ts-ignore (elastic/eui#1262) EuiFilePicker is not exported yet
import { EuiFilePicker } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { SFC } from 'react';
import React, { FunctionComponent } from 'react';
interface Props {
/** Optional ID of the component */
@ -18,7 +18,7 @@ interface Props {
onUpload: () => void;
}
export const FileUpload: SFC<Props> = props => (
export const FileUpload: FunctionComponent<Props> = props => (
<EuiFilePicker compressed id={props.id} className={props.className} onChange={props.onUpload} />
);

View file

@ -7,7 +7,7 @@
// @ts-ignore (elastic/eui#1262) EuiSuperSelect is not exported yet
import { EuiSuperSelect } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { SFC } from 'react';
import React, { FunctionComponent } from 'react';
import { fonts, FontValue } from '../../../common/lib/fonts';
interface Props {
@ -15,7 +15,7 @@ interface Props {
value?: FontValue;
}
export const FontPicker: SFC<Props> = props => {
export const FontPicker: FunctionComponent<Props> = props => {
const { value, onSelect } = props;
// While fonts are strongly-typed, we also support custom fonts someone might type in.

View file

@ -4,12 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiIcon, EuiLoadingSpinner, isColorDark } from '@elastic/eui';
import PropTypes from 'prop-types';
import { EuiLoadingSpinner, EuiIcon, isColorDark } from '@elastic/eui';
import React, { FunctionComponent } from 'react';
import { hexToRgb } from '../../../common/lib/hex_to_rgb';
export const Loading = ({ animated, text, backgroundColor }) => {
interface Props {
animated?: boolean;
backgroundColor?: string;
text?: string;
}
export const Loading: FunctionComponent<Props> = ({
animated = false,
text = '',
backgroundColor = '#000000',
}) => {
if (animated) {
return (
<div className="canvasLoading">
@ -25,6 +35,11 @@ export const Loading = ({ animated, text, backgroundColor }) => {
}
const rgb = hexToRgb(backgroundColor);
let color = 'text';
if (rgb && isColorDark(rgb[0], rgb[1], rgb[2])) {
color = 'ghost';
}
return (
<div className="canvasLoading">
@ -34,7 +49,7 @@ export const Loading = ({ animated, text, backgroundColor }) => {
&nbsp;
</span>
)}
<EuiIcon color={rgb && isColorDark(...rgb) ? 'ghost' : 'text'} type="clock" />
<EuiIcon color={color} type="clock" />
</div>
);
};

View file

@ -2425,6 +2425,11 @@
dependencies:
"@types/babel-types" "*"
"@types/base64-js@^1.2.5":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@types/base64-js/-/base64-js-1.2.5.tgz#582b2476169a6cba460a214d476c744441d873d5"
integrity sha1-WCskdhaabLpGCiFNR2x0REHYc9U=
"@types/bluebird@^3.1.1":
version "3.5.20"
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.20.tgz#f6363172add6f4eabb8cada53ca9af2781e8d6a1"
@ -2620,6 +2625,11 @@
resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-7.2.1.tgz#5630999aa75532e00af42a54cbe05e1651f4a080"
integrity sha512-zuLhLEK4gOPxhkiUhqbG4p0lKY2ePEE//5NHTTn/vjYl0XWpfk2x0Fw7EWKtCjlggEsuc1GvpasD46X9PSZFaA==
"@types/file-saver@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.0.tgz#cbb49815a5e1129d5f23836a98d65d93822409af"
integrity sha512-dxdRrUov2HVTbSRFX+7xwUPlbGYVEZK6PrSqClg2QPos3PNe0bCajkDDkDeeC1znjSH03KOEqVbXpnJuWa2wgQ==
"@types/form-data@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e"
@ -2861,6 +2871,11 @@
resolved "https://registry.yarnpkg.com/@types/mime-db/-/mime-db-1.27.0.tgz#9bc014a1fd1fdf47649c1a54c6dd7966b8284792"
integrity sha1-m8AUof0f30dknBpUxt15ZrgoR5I=
"@types/mime@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
"@types/mimos@*":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/mimos/-/mimos-3.0.1.tgz#59d96abe1c9e487e7463fe41e8d86d76b57a441a"