[pre-req] New Component Layout proposal (#72385)

* New Component Layout proposal

* Add contribution guidelines; remove dead i18n

* Re-adding i18n... ugh

* Fix i18n files to reflect changes

* Addressing feedback

* Fix merge issue

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Clint Andrew Hall 2020-07-21 19:18:57 -04:00 committed by GitHub
parent b3f1595331
commit a4957e65c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1333 additions and 473 deletions

View file

@ -0,0 +1,139 @@
# Contributing to Canvas
Canvas is a plugin for Kibana, therefore its [contribution guidelines](../../../CONTRIBUTING.md) apply to Canvas development, as well. This document contains Canvas-specific guidelines that extend from the Kibana guidelines.
- [Active Migrations](#active_migrations)
- [i18n](#i18n)
- [Component Code Structure](#component_code_structure)
- [Storybook](#storybook)
## Active Migrations
When editing code in Canvas, be aware of the following active migrations, (generally, taken when a file is touched):
- Convert file(s) to Typescript.
- Convert React classes to Functional components, (where applicable).
- Add Storybook stories for components, (and thus Storyshots).
- Remove `recompose` in favor of React hooks.
- Apply improved component structure.
- Write tests.
## i18n
i18n syntax in Kibana can be a bit verbose in code:
```js
i18n('pluginNamespace.messageId', {
defaultMessage: 'Default message string literal, {key}',
values: {
key: 'value',
},
description: 'Message context or description',
});
```
To keep the code terse, Canvas uses i18n "dictionaries": abstracted, static singletons of accessor methods which return a given string:
```js
// i18n/components.ts
export const ComponentStrings = {
// ...
AssetManager: {
getCopyAssetMessage: (id: string) =>
i18n.translate('xpack.canvas.assetModal.copyAssetMessage', {
defaultMessage: `Copied '{id}' to clipboard`,
values: {
id,
},
}),
// ...
},
// ...
};
// asset_manager.tsx
import { ComponentStrings } from '../../../i18n';
const { AssetManager: strings } = ComponentStrings;
const text = (
<EuiText>
{strings.getSpaceUsedText(percentageUsed)}
</EuiText>
);
```
These singletons can then be changed at will, as well as audited for unused methods, (and therefore unused i18n strings).
## Component Code Structure
Canvas uses Redux. Component code is divided into React components and Redux containers. This way, components can be reused, their containers can be edited, and both can be tested independently.
Canvas is actively migrating to a structure which uses the `index.ts` file as a thin exporting index, rather than functional code:
```
- components
- foo <- directory representing the component
- foo.ts <- redux container
- foo.component.tsx <- react component
- foo.scss
- index.ts <- thin exporting index, (no redux)
- bar <- directory representing the component
- bar.ts
- bar.component.tsx
- bar.scss
- bar_dep.ts <- redux sub container
- bar_dep.component.tsx <- sub component
- index.ts
```
The exporting file would be:
```
export { Foo } from './foo';
export { Foo as FooComponent } from './foo.component';
```
### Why?
Canvas has been using an "index-export" structure that has served it well, until recently. In this structure, the `index.ts` file serves as the primary export of the Redux component, (and holds that code). The component is then named-- `component.tsx`-- and consumed in the `index` file.
The problem we've run into is when you have sub-components which are also connected to Redux. To maintain this structure, each sub-component and its Redux container would then be stored in a subdirectory, (with only two files in it).
> NOTE: if a PR touches component code that is in the older structure, it should be migrated to the structure above.
## Storybook
Canvas uses [Storybook](https://storybook.js.org) to test and develop components. This has a number of benefits:
- Developing components without needing to start ES + Kibana.
- Testing components interactively without starting ES + Kibana.
- Automatic Storyshot integration with Jest
### Using Storybook
The Canvas Storybook instance can be started by running `node scripts/storybook` from the Canvas root directory. It has a number of options:
```
node scripts/storybook
Storybook runner for Canvas.
Options:
--clean Forces a clean of the Storybook DLL and exits.
--dll Cleans and builds the Storybook dependency DLL and exits.
--stats Produces a Webpack stats file.
--site Produces a site deployment of this Storybook.
--verbose, -v Log verbosely
--debug Log debug messages (less than verbose)
--quiet Only log errors
--silent Don't log anything
--help Show this message
```
### What about `kbn-storybook`?
Canvas wants to move to the Kibana Storybook instance as soon as feasible. There are few tweaks Canvas makes to Storybook, so we're actively working with the maintainers to make that migration successful.
In the meantime, people can test our progress by running `node scripts/storybook_new` from the Canvas root.

View file

@ -110,26 +110,24 @@ export const ComponentStrings = {
i18n.translate('xpack.canvas.asset.thumbnailAltText', {
defaultMessage: 'Asset thumbnail',
}),
getConfirmModalButtonLabel: () =>
i18n.translate('xpack.canvas.asset.confirmModalButtonLabel', {
defaultMessage: 'Remove',
}),
getConfirmModalMessageText: () =>
i18n.translate('xpack.canvas.asset.confirmModalDetail', {
defaultMessage: 'Are you sure you want to remove this asset?',
}),
getConfirmModalTitle: () =>
i18n.translate('xpack.canvas.asset.confirmModalTitle', {
defaultMessage: 'Remove Asset',
}),
},
AssetManager: {
getButtonLabel: () =>
i18n.translate('xpack.canvas.assetManager.manageButtonLabel', {
defaultMessage: 'Manage assets',
}),
getConfirmModalButtonLabel: () =>
i18n.translate('xpack.canvas.assetManager.confirmModalButtonLabel', {
defaultMessage: 'Remove',
}),
getConfirmModalMessageText: () =>
i18n.translate('xpack.canvas.assetManager.confirmModalDetail', {
defaultMessage: 'Are you sure you want to remove this asset?',
}),
getConfirmModalTitle: () =>
i18n.translate('xpack.canvas.assetManager.confirmModalTitle', {
defaultMessage: 'Remove Asset',
}),
},
AssetModal: {
getDescription: () =>
i18n.translate('xpack.canvas.assetModal.modalDescription', {
defaultMessage:
@ -162,6 +160,13 @@ export const ComponentStrings = {
percentageUsed,
},
}),
getCopyAssetMessage: (id: string) =>
i18n.translate('xpack.canvas.assetModal.copyAssetMessage', {
defaultMessage: `Copied '{id}' to clipboard`,
values: {
id,
},
}),
},
AssetPicker: {
getAssetAltText: () =>

View file

@ -355,3 +355,181 @@ exports[`Storyshots components/Assets/Asset marker 1`] = `
</div>
</div>
`;
exports[`Storyshots components/Assets/Asset redux 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 "
>
<img
alt="Asset thumbnail"
className="euiImage__img"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4="
style={Object {}}
/>
</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"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Create image element"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="vector"
size="m"
/>
</button>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-download"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
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"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="sortDown"
size="m"
/>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
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"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="copyClipboard"
size="m"
/>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Delete"
className="euiButtonIcon euiButtonIcon--danger"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="trash"
size="m"
/>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -229,6 +229,545 @@ Array [
]
`;
exports[`Storyshots components/Assets/AssetManager redux 1`] = `
Array [
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>,
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={1}
/>,
<div
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
>
<div
className="euiModal canvasAssetManager canvasModal--fixedSize"
onKeyDown={[Function]}
style={
Object {
"maxWidth": "1000px",
}
}
tabIndex={0}
>
<button
aria-label="Closes this modal window"
className="euiButtonIcon euiButtonIcon--text euiModal__closeIcon"
onClick={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="cross"
size="m"
/>
</button>
<div
className="euiModal__flex"
>
<div
className="euiModalHeader canvasAssetManager__modalHeader"
>
<div
className="euiModalHeader__title canvasAssetManager__modalHeaderTitle"
>
Manage workpad assets
</div>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive canvasAssetManager__fileUploadWrapper"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiFilePicker euiFilePicker--compressed"
>
<div
className="euiFilePicker__wrap"
>
<input
accept="image/*"
aria-describedby="generated-id"
className="euiFilePicker__input"
multiple={true}
onChange={[Function]}
onDragLeave={[Function]}
onDragOver={[Function]}
onDrop={[Function]}
type="file"
/>
<div
className="euiFilePicker__prompt"
id="generated-id"
>
<div
aria-hidden="true"
className="euiFilePicker__icon"
data-euiicon-type="importAction"
size="m"
/>
<div
className="euiFilePicker__promptText"
>
Select or drag and drop images
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="euiModalBody"
>
<div
className="euiModalBody__overflow"
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--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>
</div>
</div>
<div
className="euiSpacer euiSpacer--l"
/>
<div
className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive"
>
<div
className="euiFlexItem"
>
<div
className="euiPanel euiPanel--paddingSmall canvasAsset"
>
<div
className="canvasAsset__thumb canvasCheckered"
>
<figure
className="euiImage canvasAsset__img "
>
<img
alt="Asset thumbnail"
className="euiImage__img"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4="
style={Object {}}
/>
</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"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Create image element"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="vector"
size="m"
/>
</button>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-download"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
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"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="sortDown"
size="m"
/>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
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"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="copyClipboard"
size="m"
/>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Delete"
className="euiButtonIcon euiButtonIcon--danger"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="trash"
size="m"
/>
</button>
</span>
</div>
</div>
</div>
</div>
<div
className="euiFlexItem"
>
<div
className="euiPanel euiPanel--paddingSmall canvasAsset"
>
<div
className="canvasAsset__thumb canvasCheckered"
>
<figure
className="euiImage canvasAsset__img "
>
<img
alt="Asset thumbnail"
className="euiImage__img"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4="
style={Object {}}
/>
</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"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Create image element"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="vector"
size="m"
/>
</button>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-download"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
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"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="sortDown"
size="m"
/>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
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"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="copyClipboard"
size="m"
/>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Delete"
className="euiButtonIcon euiButtonIcon--danger"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="trash"
size="m"
/>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="euiModalFooter canvasAssetManager__modalFooter"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow canvasAssetManager__meterWrapper"
>
<div
className="euiFlexItem"
>
<progress
aria-labelledby="CanvasAssetManagerLabel"
className="euiProgress euiProgress--native euiProgress--s euiProgress--secondary"
max={25000}
value={2}
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero eui-textNoWrap"
>
<div
className="euiText euiText--medium"
id="CanvasAssetManagerLabel"
>
0% space used
</div>
</div>
</div>
<button
className="euiButton euiButton--primary euiButton--small"
onClick={[Function]}
type="button"
>
<span
className="euiButton__content"
>
<span
className="euiButton__text"
>
Close
</span>
</span>
</button>
</div>
</div>
</div>
</div>,
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>,
]
`;
exports[`Storyshots components/Assets/AssetManager two assets 1`] = `
Array [
<div

View file

@ -1,46 +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 { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { Asset } from '../asset';
import { AssetType } from '../../../../types';
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/Assets/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

@ -7,42 +7,32 @@
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { AssetType } from '../../../../types';
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=',
};
import { AssetManager, AssetManagerComponent } from '../';
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=',
};
import { Provider, AIRPLANE, MARKER } from './provider';
storiesOf('components/Assets/AssetManager', module)
.add('redux: AssetManager', () => (
<Provider>
<AssetManager onClose={action('onClose')} />
</Provider>
))
.add('no assets', () => (
<AssetManager
onAddImageElement={action('onAddImageElement')}
onAssetAdd={action('onAssetAdd')}
onAssetCopy={action('onAssetCopy')}
onAssetDelete={action('onAssetDelete')}
onClose={action('onClose')}
/>
<Provider>
<AssetManagerComponent
assets={[]}
onClose={action('onClose')}
onAddAsset={action('onAddAsset')}
/>
</Provider>
))
.add('two assets', () => (
<AssetManager
assetValues={[AIRPLANE, MARKER]}
onAddImageElement={action('onAddImageElement')}
onAssetAdd={action('onAssetAdd')}
onAssetCopy={action('onAssetCopy')}
onAssetDelete={action('onAssetDelete')}
onClose={action('onClose')}
/>
<Provider>
<AssetManagerComponent
assets={[AIRPLANE, MARKER]}
onClose={action('onClose')}
onAddAsset={action('onAddAsset')}
/>
</Provider>
));

View file

@ -0,0 +1,110 @@
/*
* 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.
*/
/* eslint-disable no-console */
/*
This Provider is temporary. See https://github.com/elastic/kibana/pull/69357
*/
import React, { FC } from 'react';
import { applyMiddleware, createStore, Dispatch, Store } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { Provider as ReduxProvider } from 'react-redux';
// @ts-expect-error untyped local
import { appReady } from '../../../../public/state/middleware/app_ready';
// @ts-expect-error untyped local
import { resolvedArgs } from '../../../../public/state/middleware/resolved_args';
// @ts-expect-error untyped local
import { getRootReducer } from '../../../../public/state/reducers';
// @ts-expect-error Untyped local
import { getDefaultWorkpad } from '../../../../public/state/defaults';
import { State, AssetType } from '../../../../types';
export 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=',
};
export 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=',
};
export const state: State = {
app: {
basePath: '/',
ready: true,
serverFunctions: [],
},
assets: {
AIRPLANE,
MARKER,
},
transient: {
canUserWrite: true,
zoomScale: 1,
elementStats: {
total: 0,
ready: 0,
pending: 0,
error: 0,
},
inFlight: false,
fullScreen: false,
selectedTopLevelNodes: [],
resolvedArgs: {},
refresh: {
interval: 0,
},
autoplay: {
enabled: false,
interval: 10000,
},
},
persistent: {
schemaVersion: 2,
workpad: getDefaultWorkpad(),
},
};
// @ts-expect-error untyped local
import { elementsRegistry } from '../../../lib/elements_registry';
import { image } from '../../../../canvas_plugin_src/elements/image';
elementsRegistry.register(image);
export const patchDispatch: (store: Store, dispatch: Dispatch) => Dispatch = (store, dispatch) => (
action
) => {
const previousState = store.getState();
const returnValue = dispatch(action);
const newState = store.getState();
console.group(action.type || '(thunk)');
console.log('Previous State', previousState);
console.log('New State', newState);
console.groupEnd();
return returnValue;
};
export const Provider: FC = ({ children }) => {
const middleware = applyMiddleware(thunkMiddleware);
const reducer = getRootReducer(state);
const store = createStore(reducer, state, middleware);
store.dispatch = patchDispatch(store, store.dispatch);
return <ReduxProvider store={store}>{children}</ReduxProvider>;
};

View file

@ -0,0 +1,147 @@
/*
* 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, { FC, useState } from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
EuiPanel,
EuiSpacer,
EuiText,
EuiTextColor,
EuiToolTip,
} from '@elastic/eui';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { ConfirmModal } from '../confirm_modal';
import { Clipboard } from '../clipboard';
import { Download } from '../download';
import { AssetType } from '../../../types';
import { ComponentStrings } from '../../../i18n';
const { Asset: strings } = ComponentStrings;
interface Props {
/** The asset to be rendered */
asset: AssetType;
/** The function to execute when the user clicks 'Create' */
onCreate: (assetId: string) => void;
/** The function to execute when the user clicks 'Delete' */
onDelete: (asset: AssetType) => void;
}
export const Asset: FC<Props> = ({ asset, onCreate, onDelete }) => {
const { services } = useKibana();
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
const onCopy = (result: boolean) =>
result && services.canvas.notify.success(`Copied '${asset.id}' to clipboard`);
const confirmModal = (
<ConfirmModal
isOpen={isConfirmModalVisible}
title={strings.getConfirmModalTitle()}
message={strings.getConfirmModalMessageText()}
confirmButtonText={strings.getConfirmModalButtonLabel()}
onConfirm={() => {
setIsConfirmModalVisible(false);
onDelete(asset);
}}
onCancel={() => setIsConfirmModalVisible(false)}
/>
);
const createImage = (
<EuiFlexItem className="asset-create-image" grow={false}>
<EuiToolTip content={strings.getCreateImageTooltip()}>
<EuiButtonIcon
iconType="vector"
aria-label={strings.getCreateImageTooltip()}
onClick={() => onCreate(asset.id)}
/>
</EuiToolTip>
</EuiFlexItem>
);
const downloadAsset = (
<EuiFlexItem className="asset-download" grow={false}>
<EuiToolTip content={strings.getDownloadAssetTooltip()}>
<Download fileName={asset.id} content={asset.value}>
<EuiButtonIcon iconType="sortDown" aria-label={strings.getDownloadAssetTooltip()} />
</Download>
</EuiToolTip>
</EuiFlexItem>
);
const copyAsset = (
<EuiFlexItem grow={false}>
<EuiToolTip content={strings.getCopyAssetTooltip()}>
<Clipboard content={asset.id} onCopy={onCopy}>
<EuiButtonIcon iconType="copyClipboard" aria-label={strings.getCopyAssetTooltip()} />
</Clipboard>
</EuiToolTip>
</EuiFlexItem>
);
const deleteAsset = (
<EuiFlexItem grow={false}>
<EuiToolTip content={strings.getDeleteAssetTooltip()}>
<EuiButtonIcon
color="danger"
iconType="trash"
aria-label={strings.getDeleteAssetTooltip()}
onClick={() => setIsConfirmModalVisible(true)}
/>
</EuiToolTip>
</EuiFlexItem>
);
const thumbnail = (
<div className="canvasAsset__thumb canvasCheckered">
<EuiImage
className="canvasAsset__img"
size="original"
url={asset.value}
fullScreenIconColor="dark"
alt={strings.getThumbnailAltText()}
/>
</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={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>
{isConfirmModalVisible ? confirmModal : null}
</EuiFlexItem>
);
};

View file

@ -3,124 +3,59 @@
* 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,
EuiImage,
EuiPanel,
EuiSpacer,
EuiText,
EuiTextColor,
EuiToolTip,
} from '@elastic/eui';
import React, { FunctionComponent } from 'react';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { set } from '@elastic/safer-lodash-set';
import { ComponentStrings } from '../../../i18n';
import { fromExpression, toExpression } from '@kbn/interpreter/common';
import { Clipboard } from '../clipboard';
import { Download } from '../download';
import { AssetType } from '../../../types';
// @ts-expect-error untyped local
import { elementsRegistry } from '../../lib/elements_registry';
// @ts-expect-error untyped local
import { addElement } from '../../state/actions/elements';
import { getSelectedPage } from '../../state/selectors/workpad';
// @ts-expect-error untyped local
import { removeAsset } from '../../state/actions/assets';
import { State, ExpressionAstExpression, AssetType } from '../../../types';
const { Asset: strings } = ComponentStrings;
import { Asset as Component } from './asset.component';
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 = connect(
(state: State) => ({
selectedPage: getSelectedPage(state),
}),
(dispatch: Dispatch) => ({
onCreate: (pageId: string) => (assetId: string) => {
const imageElement = elementsRegistry.get('image');
const elementAST = fromExpression(imageElement.expression);
const selector = ['chain', '0', 'arguments', 'dataurl'];
const subExp: ExpressionAstExpression[] = [
{
type: 'expression',
chain: [
{
type: 'function',
function: 'asset',
arguments: {
_: [assetId],
},
},
],
},
];
const newAST = set<ExpressionAstExpression>(elementAST, selector, subExp);
imageElement.expression = toExpression(newAST);
dispatch(addElement(pageId, imageElement));
},
onDelete: (asset: AssetType) => dispatch(removeAsset(asset.id)),
}),
(stateProps, dispatchProps, ownProps) => {
const { onCreate, onDelete } = dispatchProps;
export const Asset: FunctionComponent<Props> = (props) => {
const { asset, onCreate, onCopy, onDelete } = props;
const createImage = (
<EuiFlexItem className="asset-create-image" grow={false}>
<EuiToolTip content={strings.getCreateImageTooltip()}>
<EuiButtonIcon
iconType="vector"
aria-label={strings.getCreateImageTooltip()}
onClick={() => onCreate(asset)}
/>
</EuiToolTip>
</EuiFlexItem>
);
const downloadAsset = (
<EuiFlexItem className="asset-download" grow={false}>
<EuiToolTip content={strings.getDownloadAssetTooltip()}>
<Download fileName={asset.id} content={asset.value}>
<EuiButtonIcon iconType="sortDown" aria-label={strings.getDownloadAssetTooltip()} />
</Download>
</EuiToolTip>
</EuiFlexItem>
);
const copyAsset = (
<EuiFlexItem grow={false}>
<EuiToolTip content={strings.getCopyAssetTooltip()}>
<Clipboard content={asset.id} onCopy={(result: boolean) => result && onCopy(asset)}>
<EuiButtonIcon iconType="copyClipboard" aria-label={strings.getCopyAssetTooltip()} />
</Clipboard>
</EuiToolTip>
</EuiFlexItem>
);
const deleteAsset = (
<EuiFlexItem grow={false}>
<EuiToolTip content={strings.getDeleteAssetTooltip()}>
<EuiButtonIcon
color="danger"
iconType="trash"
aria-label={strings.getDeleteAssetTooltip()}
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={strings.getThumbnailAltText()}
/>
</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>
);
};
return {
...ownProps,
onCreate: onCreate(stateProps.selectedPage),
onDelete,
};
}
)(Component);

View file

@ -3,6 +3,9 @@
* 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, { FC, useState } from 'react';
import PropTypes from 'prop-types';
import {
EuiButton,
EuiEmptyPrompt,
@ -21,48 +24,29 @@ import {
EuiSpacer,
EuiText,
} from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import { ComponentStrings } from '../../../i18n';
import { ASSET_MAX_SIZE } from '../../../common/lib/constants';
import { Loading } from '../loading';
import { Asset } from './asset';
import { AssetType } from '../../../types';
import { ComponentStrings } from '../../../i18n';
const { AssetModal: strings } = ComponentStrings;
const { AssetManager: strings } = ComponentStrings;
interface Props {
/** The assets to display within the modal */
assetValues: AssetType[];
/** Indicates if assets are being loaded */
isLoading: boolean;
assets: AssetType[];
/** Function to invoke when the modal is closed */
onClose: () => void;
/** Function to invoke when a file is uploaded */
onFileUpload: (assets: FileList | null) => 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;
onAddAsset: (file: File) => void;
}
export const AssetModal: FunctionComponent<Props> = (props) => {
const {
assetValues,
isLoading,
onAssetCopy,
onAssetCreate,
onAssetDelete,
onClose,
onFileUpload,
} = props;
export const AssetManager: FC<Props> = (props) => {
const { assets, onClose, onAddAsset } = props;
const [isLoading, setIsLoading] = useState(false);
const assetsTotal = Math.round(
assetValues.reduce((total, { value }) => total + value.length, 0) / 1024
assets.reduce((total, { value }) => total + value.length, 0) / 1024
);
const percentageUsed = Math.round((assetsTotal / ASSET_MAX_SIZE) * 100);
@ -77,10 +61,22 @@ export const AssetModal: FunctionComponent<Props> = (props) => {
</EuiPanel>
);
const onFileUpload = (files: FileList | null) => {
if (files === null) {
return;
}
setIsLoading(true);
Promise.all(Array.from(files).map((file) => onAddAsset(file))).finally(() => {
setIsLoading(false);
});
};
return (
<EuiOverlayMask>
<EuiModal
onClose={onClose}
onClose={() => onClose()}
className="canvasAssetManager canvasModal--fixedSize"
maxWidth="1000px"
>
@ -110,16 +106,10 @@ export const AssetModal: FunctionComponent<Props> = (props) => {
<p>{strings.getDescription()}</p>
</EuiText>
<EuiSpacer />
{assetValues.length ? (
{assets.length ? (
<EuiFlexGrid columns={4}>
{assetValues.map((asset) => (
<Asset
asset={asset}
key={asset.id}
onCopy={onAssetCopy}
onCreate={onAssetCreate}
onDelete={onAssetDelete}
/>
{assets.map((asset) => (
<Asset asset={asset} key={asset.id} />
))}
</EuiFlexGrid>
) : (
@ -143,7 +133,7 @@ export const AssetModal: FunctionComponent<Props> = (props) => {
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiButton size="s" onClick={onClose}>
<EuiButton size="s" onClick={() => onClose()}>
{strings.getModalCloseButtonLabel()}
</EuiButton>
</EuiModalFooter>
@ -152,12 +142,8 @@ export const AssetModal: FunctionComponent<Props> = (props) => {
);
};
AssetModal.propTypes = {
assetValues: PropTypes.array,
isLoading: PropTypes.bool,
AssetManager.propTypes = {
assets: PropTypes.arrayOf(PropTypes.object).isRequired,
onClose: PropTypes.func.isRequired,
onFileUpload: PropTypes.func.isRequired,
onAssetCopy: PropTypes.func.isRequired,
onAssetCreate: PropTypes.func.isRequired,
onAssetDelete: PropTypes.func.isRequired,
onAddAsset: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,70 @@
/*
* 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 { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { get } from 'lodash';
import { getId } from '../../lib/get_id';
// @ts-expect-error untyped local
import { findExistingAsset } from '../../lib/find_existing_asset';
import { VALID_IMAGE_TYPES } from '../../../common/lib/constants';
import { encode } from '../../../common/lib/dataurl';
// @ts-expect-error untyped local
import { elementsRegistry } from '../../lib/elements_registry';
// @ts-expect-error untyped local
import { addElement } from '../../state/actions/elements';
import { getAssets } from '../../state/selectors/assets';
// @ts-expect-error untyped local
import { removeAsset, createAsset } from '../../state/actions/assets';
import { State, AssetType } from '../../../types';
import { AssetManager as Component } from './asset_manager.component';
export const AssetManager = connect(
(state: State) => ({
assets: getAssets(state),
}),
(dispatch: Dispatch) => ({
onAddAsset: (type: string, content: string) => {
// make the ID here and pass it into the action
const assetId = getId('asset');
dispatch(createAsset(type, content, assetId));
// then return the id, so the caller knows the id that will be created
return assetId;
},
}),
(stateProps, dispatchProps, ownProps) => {
const { assets } = stateProps;
const { onAddAsset } = dispatchProps;
// pull values out of assets object
// have to cast to AssetType[] because TS doesn't know about filtering
const assetValues = Object.values(assets).filter((asset) => !!asset) as AssetType[];
return {
...ownProps,
assets: assetValues,
onAddAsset: (file: File) => {
const [type, subtype] = get(file, 'type', '').split('/');
if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) {
return encode(file).then((dataurl) => {
const dataurlType = 'dataurl';
const existingId = findExistingAsset(dataurlType, dataurl, assetValues);
if (existingId) {
return existingId;
}
return onAddAsset(dataurlType, dataurl);
});
}
return false;
},
};
}
)(Component);

View file

@ -1,111 +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 PropTypes from 'prop-types';
import React, { Fragment, PureComponent } from 'react';
import { ComponentStrings } from '../../../i18n';
import { ConfirmModal } from '../confirm_modal';
import { AssetType } from '../../../types';
import { AssetModal } from './asset_modal';
const { AssetManager: strings } = ComponentStrings;
export 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;
/** Function to invoke when an asset modal is closed */
onClose: () => void;
}
interface State {
/** The id of the asset to delete, if applicable. Is set if the viewer clicks the delete icon */
deleteId: string | null;
/** 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,
onClose: PropTypes.func.isRequired,
};
public static defaultProps = {
assetValues: [],
};
public state = {
deleteId: null,
isLoading: false,
};
public render() {
const { isLoading } = this.state;
const { assetValues, onAssetCopy, onAddImageElement, onClose } = this.props;
const assetModal = (
<AssetModal
assetValues={assetValues}
isLoading={isLoading}
onAssetCopy={onAssetCopy}
onAssetCreate={(createdAsset: AssetType) => {
onAddImageElement(createdAsset.id);
onClose();
}}
onAssetDelete={(asset: AssetType) => this.setState({ deleteId: asset.id })}
onClose={onClose}
onFileUpload={this.handleFileUpload}
/>
);
const confirmModal = (
<ConfirmModal
isOpen={this.state.deleteId !== null}
title={strings.getConfirmModalTitle()}
message={strings.getConfirmModalMessageText()}
confirmButtonText={strings.getConfirmModalButtonLabel()}
onConfirm={this.doDelete}
onCancel={this.resetDelete}
/>
);
return (
<Fragment>
{assetModal}
{confirmModal}
</Fragment>
);
}
private resetDelete = () => this.setState({ deleteId: null });
private doDelete = () => {
this.resetDelete();
this.props.onAssetDelete(this.state.deleteId);
};
private handleFileUpload = (files: FileList | null) => {
if (files == null) return;
this.setState({ isLoading: true });
Promise.all(Array.from(files).map((file) => this.props.onAssetAdd(file))).finally(() => {
this.setState({ isLoading: false });
});
};
}

View file

@ -4,107 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { connect } from 'react-redux';
import { compose, withProps } from 'recompose';
import { set } from '@elastic/safer-lodash-set';
import { get } from 'lodash';
import { fromExpression, toExpression } from '@kbn/interpreter/common';
import { getAssets } from '../../state/selectors/assets';
// @ts-expect-error untyped local
import { removeAsset, createAsset } from '../../state/actions/assets';
// @ts-expect-error untyped local
import { elementsRegistry } from '../../lib/elements_registry';
// @ts-expect-error untyped local
import { addElement } from '../../state/actions/elements';
import { getSelectedPage } from '../../state/selectors/workpad';
import { encode } from '../../../common/lib/dataurl';
import { getId } from '../../lib/get_id';
// @ts-expect-error untyped local
import { findExistingAsset } from '../../lib/find_existing_asset';
import { VALID_IMAGE_TYPES } from '../../../common/lib/constants';
import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
import { WithKibanaProps } from '../../';
import { AssetManager as Component, Props as AssetManagerProps } from './asset_manager';
import { State, ExpressionAstExpression, AssetType } from '../../../types';
const mapStateToProps = (state: State) => ({
assets: getAssets(state),
selectedPage: getSelectedPage(state),
});
const mapDispatchToProps = (dispatch: (action: any) => void) => ({
onAddImageElement: (pageId: string) => (assetId: string) => {
const imageElement = elementsRegistry.get('image');
const elementAST = fromExpression(imageElement.expression);
const selector = ['chain', '0', 'arguments', 'dataurl'];
const subExp: ExpressionAstExpression[] = [
{
type: 'expression',
chain: [
{
type: 'function',
function: 'asset',
arguments: {
_: [assetId],
},
},
],
},
];
const newAST = set<ExpressionAstExpression>(elementAST, selector, subExp);
imageElement.expression = toExpression(newAST);
dispatch(addElement(pageId, imageElement));
},
onAssetAdd: (type: string, content: string) => {
// make the ID here and pass it into the action
const assetId = getId('asset');
dispatch(createAsset(type, content, assetId));
// then return the id, so the caller knows the id that will be created
return assetId;
},
onAssetDelete: (assetId: string) => dispatch(removeAsset(assetId)),
});
const mergeProps = (
stateProps: ReturnType<typeof mapStateToProps>,
dispatchProps: ReturnType<typeof mapDispatchToProps>,
ownProps: AssetManagerProps
) => {
const { assets, selectedPage } = stateProps;
const { onAssetAdd } = dispatchProps;
const assetValues = Object.values(assets); // pull values out of assets object
return {
...ownProps,
...dispatchProps,
onAddImageElement: dispatchProps.onAddImageElement(stateProps.selectedPage),
selectedPage,
assetValues,
onAssetAdd: (file: File) => {
const [type, subtype] = get(file, 'type', '').split('/');
if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) {
return encode(file).then((dataurl) => {
const dataurlType = 'dataurl';
const existingId = findExistingAsset(dataurlType, dataurl, assetValues);
if (existingId) {
return existingId;
}
return onAssetAdd(dataurlType, dataurl);
});
}
return false;
},
};
};
export const AssetManager = compose<any, any>(
connect(mapStateToProps, mapDispatchToProps, mergeProps),
withKibana,
withProps(({ kibana }: WithKibanaProps) => ({
onAssetCopy: (asset: AssetType) =>
kibana.services.canvas.notify.success(`Copied '${asset.id}' to clipboard`),
}))
)(Component);
export { Asset } from './asset';
export { Asset as AssetComponent } from './asset.component';
export { AssetManager } from './asset_manager';
export { AssetManager as AssetManagerComponent } from './asset_manager.component';

View file

@ -94,4 +94,5 @@ addSerializer(styleSheetSerializer);
initStoryshots({
configPath: path.resolve(__dirname, './../storybook'),
test: multiSnapshotWithOptions({}),
storyNameRegex: /^((?!.*?redux).)*$/,
});

View file

@ -47,6 +47,12 @@ module.exports = async ({ config }) => {
],
});
config.module.rules.push({
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
});
// Parse props data for .tsx files
// This is notoriously slow, and is making Storybook unusable. Disabling for now.
// See: https://github.com/storybookjs/storybook/issues/7998
@ -117,6 +123,15 @@ module.exports = async ({ config }) => {
],
});
// Exclude large-dependency modules that need not be included in Storybook.
config.module.rules.push({
test: [
path.resolve(__dirname, '../public/components/embeddable_flyout'),
path.resolve(__dirname, '../../reporting/public'),
],
use: 'null-loader',
});
// Ensure jQuery is global for Storybook, specifically for the runtime.
config.plugins.push(
new webpack.ProvidePlugin({
@ -216,5 +231,7 @@ module.exports = async ({ config }) => {
config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public');
config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock');
config.resolve.extensions.push('.mjs');
return config;
};

View file

@ -4685,14 +4685,14 @@
"xpack.canvas.argFormArgSimpleForm.requiredTooltip": "この引数は必須です。数値を入力してください。",
"xpack.canvas.argFormPendingArgValue.loadingMessage": "読み込み中",
"xpack.canvas.argFormSimpleFailure.failureTooltip": "この引数のインターフェースが値を解析できなかったため、フォールバックインプットが使用されています",
"xpack.canvas.asset.confirmModalButtonLabel": "削除",
"xpack.canvas.asset.confirmModalDetail": "このアセットを削除してよろしいですか?",
"xpack.canvas.asset.confirmModalTitle": "アセットの削除",
"xpack.canvas.asset.copyAssetTooltip": "ID をクリップボードにコピー",
"xpack.canvas.asset.createImageTooltip": "画像エレメントを作成",
"xpack.canvas.asset.deleteAssetTooltip": "削除",
"xpack.canvas.asset.downloadAssetTooltip": "ダウンロード",
"xpack.canvas.asset.thumbnailAltText": "アセットのサムネイル",
"xpack.canvas.assetManager.confirmModalButtonLabel": "削除",
"xpack.canvas.assetManager.confirmModalDetail": "このアセットを削除してよろしいですか?",
"xpack.canvas.assetManager.confirmModalTitle": "アセットの削除",
"xpack.canvas.assetManager.manageButtonLabel": "アセットの管理",
"xpack.canvas.assetModal.emptyAssetsDescription": "アセットをインポートして開始します",
"xpack.canvas.assetModal.filePickerPromptText": "画像を選択するかドラッグ &amp;amp; ドロップしてください",

View file

@ -4689,14 +4689,14 @@
"xpack.canvas.argFormArgSimpleForm.requiredTooltip": "此参数为必需,应指定值。",
"xpack.canvas.argFormPendingArgValue.loadingMessage": "正在加载",
"xpack.canvas.argFormSimpleFailure.failureTooltip": "此参数的接口无法解析该值,因此将使用回退输入",
"xpack.canvas.asset.confirmModalButtonLabel": "删除",
"xpack.canvas.asset.confirmModalDetail": "确定要删除此资产?",
"xpack.canvas.asset.confirmModalTitle": "删除资产",
"xpack.canvas.asset.copyAssetTooltip": "将 ID 复制到剪贴板",
"xpack.canvas.asset.createImageTooltip": "创建图像元素",
"xpack.canvas.asset.deleteAssetTooltip": "删除",
"xpack.canvas.asset.downloadAssetTooltip": "下载",
"xpack.canvas.asset.thumbnailAltText": "资产缩略图",
"xpack.canvas.assetManager.confirmModalButtonLabel": "删除",
"xpack.canvas.assetManager.confirmModalDetail": "确定要删除此资产?",
"xpack.canvas.assetManager.confirmModalTitle": "删除资产",
"xpack.canvas.assetManager.manageButtonLabel": "管理资产",
"xpack.canvas.assetModal.emptyAssetsDescription": "导入您的资产以开始",
"xpack.canvas.assetModal.filePickerPromptText": "选择或拖放图像",