mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[6.x] Feat: Bulk Upload Assets (#29007)
This commit is contained in:
parent
b5d00bbbfe
commit
5323a57fb5
13 changed files with 166 additions and 96 deletions
|
@ -5,11 +5,21 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiFilePicker } from '@elastic/eui';
|
||||
import { Loading } from '../../../../../public/components/loading/loading';
|
||||
import { FileUpload } from '../../../../../public/components/file_upload';
|
||||
|
||||
export const FileForm = ({ loading, onUpload }) =>
|
||||
loading ? <Loading animated text="Image uploading" /> : <FileUpload onUpload={onUpload} />;
|
||||
export const FileForm = ({ loading, onChange }) =>
|
||||
loading ? (
|
||||
<Loading animated text="Image uploading" />
|
||||
) : (
|
||||
<EuiFilePicker
|
||||
initialPromptText="Select or drag and drop an image"
|
||||
onChange={onChange}
|
||||
compressed
|
||||
className="canvasImageUpload"
|
||||
accept="image/*"
|
||||
/>
|
||||
);
|
||||
|
||||
FileForm.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiSpacer, EuiButtonGroup } from '@elastic/eui';
|
||||
import { get } from 'lodash';
|
||||
import { AssetPicker } from '../../../../public/components/asset_picker';
|
||||
import { elasticOutline } from '../../../lib/elastic_outline';
|
||||
import { resolveFromArgs } from '../../../../common/lib/resolve_dataurl';
|
||||
|
@ -14,6 +15,7 @@ import { isValidHttpUrl } from '../../../../common/lib/httpurl';
|
|||
import { encode } from '../../../../common/lib/dataurl';
|
||||
import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component';
|
||||
import './image_upload.scss';
|
||||
import { VALID_IMAGE_TYPES } from '../../../../common/lib/constants';
|
||||
import { FileForm, LinkForm } from './forms';
|
||||
|
||||
class ImageUpload extends React.Component {
|
||||
|
@ -71,17 +73,21 @@ class ImageUpload extends React.Component {
|
|||
|
||||
handleUpload = files => {
|
||||
const { onAssetAdd } = this.props;
|
||||
const [upload] = files;
|
||||
this.setState({ loading: true }); // start loading indicator
|
||||
const [file] = files;
|
||||
|
||||
encode(upload)
|
||||
.then(dataurl => onAssetAdd('dataurl', dataurl))
|
||||
.then(assetId => {
|
||||
this.updateAST(assetId);
|
||||
const [type, subtype] = get(file, 'type', '').split('/');
|
||||
if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) {
|
||||
this.setState({ loading: true }); // start loading indicator
|
||||
|
||||
// this component can go away when onValueChange is called, check for _isMounted
|
||||
this._isMounted && this.setState({ loading: false }); // set loading state back to false
|
||||
});
|
||||
encode(file)
|
||||
.then(dataurl => onAssetAdd('dataurl', dataurl))
|
||||
.then(assetId => {
|
||||
this.updateAST(assetId);
|
||||
|
||||
// this component can go away when onValueChange is called, check for _isMounted
|
||||
this._isMounted && this.setState({ loading: false }); // set loading state back to false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
changeUrlType = optionId => {
|
||||
|
@ -119,7 +125,7 @@ class ImageUpload extends React.Component {
|
|||
);
|
||||
|
||||
const forms = {
|
||||
file: <FileForm loading={loading} onUpload={this.handleUpload} />,
|
||||
file: <FileForm loading={loading} onChange={this.handleUpload} />,
|
||||
link: (
|
||||
<LinkForm
|
||||
url={url}
|
||||
|
|
|
@ -18,3 +18,4 @@ export const FETCH_TIMEOUT = 30000; // 30 seconds
|
|||
export const CANVAS_USAGE_TYPE = 'canvas';
|
||||
export const SECURITY_AUTH_MESSAGE = 'Authentication failed';
|
||||
export const DEFAULT_WORKPAD_CSS = '.canvasPage {\n\n}';
|
||||
export const VALID_IMAGE_TYPES = ['gif', 'jpeg', 'png', 'svg+xml'];
|
||||
|
|
|
@ -26,24 +26,31 @@ import {
|
|||
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';
|
||||
|
||||
export class AssetManager extends React.PureComponent {
|
||||
static propTypes = {
|
||||
assets: PropTypes.array,
|
||||
addImageElement: PropTypes.func,
|
||||
removeAsset: PropTypes.func,
|
||||
copyAsset: 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 });
|
||||
|
||||
|
@ -52,6 +59,13 @@ export class AssetManager extends React.PureComponent {
|
|||
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);
|
||||
};
|
||||
|
@ -132,16 +146,32 @@ export class AssetManager extends React.PureComponent {
|
|||
);
|
||||
|
||||
render() {
|
||||
const { isModalVisible } = this.state;
|
||||
const { isModalVisible, loading } = this.state;
|
||||
const { assets } = this.props;
|
||||
|
||||
const assetMaxLimit = 25000;
|
||||
|
||||
const assetsTotal = Math.round(
|
||||
this.props.assets.reduce((total, asset) => total + asset.value.length, 0) / 1024
|
||||
assets.reduce((total, asset) => total + asset.value.length, 0) / 1024
|
||||
);
|
||||
|
||||
const percentageUsed = Math.round((assetsTotal / assetMaxLimit) * 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
|
||||
|
@ -153,6 +183,40 @@ export class AssetManager extends React.PureComponent {
|
|||
<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 />
|
||||
{assets.length ? (
|
||||
<EuiFlexGrid responsive={false} columns={4}>
|
||||
{assets.map(this.renderAsset)}
|
||||
</EuiFlexGrid>
|
||||
) : (
|
||||
emptyAssets
|
||||
)}
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter className="canvasAssetManager__modalFooter">
|
||||
<EuiFlexGroup className="canvasAssetManager__meterWrapper" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiProgress
|
||||
|
@ -167,20 +231,6 @@ export class AssetManager extends React.PureComponent {
|
|||
<EuiText id="CanvasAssetManagerLabel">{percentageUsed}% space used</EuiText>
|
||||
</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>
|
||||
<EuiFlexGrid responsive={false} columns={4}>
|
||||
{this.props.assets.map(this.renderAsset)}
|
||||
</EuiFlexGrid>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton size="s" onClick={this.closeModal}>
|
||||
Close
|
||||
</EuiButton>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.canvasAssetManager {
|
||||
|
||||
.canvasAssetManager__modalHeader {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
@ -15,8 +14,6 @@
|
|||
flex-grow: 0;
|
||||
min-width: 40%;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: $euiSize;
|
||||
|
||||
@include euiBreakpoint('xs', 's') {
|
||||
flex-grow: 1;
|
||||
|
@ -27,6 +24,11 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.canvasAssetManager__fileUploadWrapper {
|
||||
justify-content: flex-end;
|
||||
padding-right: $euiSize;
|
||||
}
|
||||
|
||||
// ASSETS LIST
|
||||
|
||||
.canvasAssetManager__asset {
|
||||
|
@ -34,6 +36,11 @@
|
|||
overflow: hidden; // hides image from outer panel boundaries
|
||||
}
|
||||
|
||||
.canvasAssetManager__emptyPanel {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.canvasAssetManager__thumb {
|
||||
margin: -$euiSizeS;
|
||||
margin-bottom: 0;
|
||||
|
@ -52,4 +59,8 @@
|
|||
opacity: 0; // only show the background image (which will properly keep proportions)
|
||||
}
|
||||
}
|
||||
|
||||
.canvasAssetManager__modalFooter {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,18 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, withProps } from 'recompose';
|
||||
import { set } from 'lodash';
|
||||
import { set, get } from 'lodash';
|
||||
import { fromExpression, toExpression } from '@kbn/interpreter/common';
|
||||
import { notify } from '../../lib/notify';
|
||||
import { getAssets } from '../../state/selectors/assets';
|
||||
import { removeAsset } from '../../state/actions/assets';
|
||||
import { removeAsset, createAsset } from '../../state/actions/assets';
|
||||
import { elementsRegistry } from '../../lib/elements_registry';
|
||||
import { addElement } from '../../state/actions/elements';
|
||||
import { getSelectedPage } from '../../state/selectors/workpad';
|
||||
import { encode } from '../../../common/lib/dataurl';
|
||||
import { getId } from '../../lib/get_id';
|
||||
import { findExistingAsset } from '../../lib/find_existing_asset';
|
||||
import { VALID_IMAGE_TYPES } from '../../../common/lib/constants';
|
||||
import { AssetManager as Component } from './asset_manager';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
@ -44,15 +48,40 @@ const mapDispatchToProps = dispatch => ({
|
|||
imageElement.expression = toExpression(newAST);
|
||||
dispatch(addElement(pageId, imageElement));
|
||||
},
|
||||
onAssetAdd: (type, content) => {
|
||||
// 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;
|
||||
},
|
||||
removeAsset: assetId => dispatch(removeAsset(assetId)),
|
||||
});
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { assets } = stateProps;
|
||||
const { onAssetAdd } = dispatchProps;
|
||||
return {
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
addImageElement: dispatchProps.addImageElement(stateProps.selectedPage),
|
||||
onAssetAdd: file => {
|
||||
const [type, subtype] = get(file, 'type', '').split('/');
|
||||
if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) {
|
||||
return encode(file).then(dataurl => {
|
||||
const type = 'dataurl';
|
||||
const existingId = findExistingAsset(type, dataurl, assets);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
return onAssetAdd(type, dataurl);
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,19 +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 { EuiFilePicker } from '@elastic/eui';
|
||||
|
||||
export const FileUpload = ({ id = '', className = 'canvasFileUpload', onUpload }) => (
|
||||
<EuiFilePicker compressed id={id} className={className} onChange={onUpload} />
|
||||
);
|
||||
|
||||
FileUpload.propTypes = {
|
||||
id: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
};
|
|
@ -21,6 +21,7 @@ import {
|
|||
getContextForIndex,
|
||||
} from '../../state/selectors/workpad';
|
||||
import { getAssets } from '../../state/selectors/assets';
|
||||
import { findExistingAsset } from '../../lib/find_existing_asset';
|
||||
import { FunctionForm as Component } from './function_form';
|
||||
|
||||
const mapStateToProps = (state, { expressionIndex }) => ({
|
||||
|
@ -93,9 +94,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
|||
onValueAdd: addArgument(element, pageId),
|
||||
onValueRemove: deleteArgument(element, pageId),
|
||||
onAssetAdd: (type, content) => {
|
||||
const existingId = Object.keys(assets).find(
|
||||
assetId => assets[assetId].type === type && assets[assetId].value === content
|
||||
);
|
||||
const existingId = findExistingAsset(type, content, assets);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import { connect } from 'react-redux';
|
|||
import { canUserWrite } from '../../state/selectors/app';
|
||||
import { getWorkpadName, getSelectedPage, isWriteable } from '../../state/selectors/workpad';
|
||||
import { setWriteable } from '../../state/actions/workpad';
|
||||
import { getAssets } from '../../state/selectors/assets';
|
||||
import { addElement } from '../../state/actions/elements';
|
||||
import { WorkpadHeader as Component } from './workpad_header';
|
||||
|
||||
|
@ -18,7 +17,6 @@ const mapStateToProps = state => ({
|
|||
canUserWrite: canUserWrite(state),
|
||||
workpadName: getWorkpadName(state),
|
||||
selectedPage: getSelectedPage(state),
|
||||
hasAssets: Object.keys(getAssets(state)).length ? true : false,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
|
|
@ -27,7 +27,6 @@ export const WorkpadHeader = ({
|
|||
isWriteable,
|
||||
canUserWrite,
|
||||
toggleWriteable,
|
||||
hasAssets,
|
||||
addElement,
|
||||
setShowElementModal,
|
||||
showElementModal,
|
||||
|
@ -115,11 +114,9 @@ export const WorkpadHeader = ({
|
|||
{isWriteable ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
{hasAssets && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssetManager />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssetManager />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
|
@ -141,7 +138,6 @@ export const WorkpadHeader = ({
|
|||
WorkpadHeader.propTypes = {
|
||||
isWriteable: PropTypes.bool,
|
||||
toggleWriteable: PropTypes.func,
|
||||
hasAssets: PropTypes.bool,
|
||||
addElement: PropTypes.func.isRequired,
|
||||
showElementModal: PropTypes.bool,
|
||||
setShowElementModal: PropTypes.func,
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
EuiButton,
|
||||
EuiToolTip,
|
||||
EuiEmptyPrompt,
|
||||
EuiFilePicker,
|
||||
} from '@elastic/eui';
|
||||
import { sortByOrder } from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
@ -25,7 +26,7 @@ import { Paginate } from '../paginate';
|
|||
import { WorkpadDropzone } from './workpad_dropzone';
|
||||
import { WorkpadCreate } from './workpad_create';
|
||||
import { WorkpadSearch } from './workpad_search';
|
||||
import { WorkpadUpload } from './workpad_upload';
|
||||
import { uploadWorkpad } from './upload_workpad';
|
||||
|
||||
const formatDate = date => date && moment(date).format('MMM D, YYYY @ h:mma');
|
||||
|
||||
|
@ -79,7 +80,7 @@ export class WorkpadLoader extends React.PureComponent {
|
|||
};
|
||||
|
||||
// create new workpad from uploaded JSON
|
||||
uploadWorkpad = async workpad => {
|
||||
onUpload = async workpad => {
|
||||
this.setState({ createPending: true });
|
||||
await this.props.createWorkpad(workpad);
|
||||
this._isMounted && this.setState({ createPending: false });
|
||||
|
@ -232,7 +233,7 @@ export class WorkpadLoader extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<Fragment>
|
||||
<WorkpadDropzone onUpload={this.uploadWorkpad} disabled={createPending || !canUserWrite}>
|
||||
<WorkpadDropzone onUpload={this.onUpload} disabled={createPending || !canUserWrite}>
|
||||
<EuiBasicTable
|
||||
compressed
|
||||
items={rows}
|
||||
|
@ -294,7 +295,14 @@ export class WorkpadLoader extends React.PureComponent {
|
|||
);
|
||||
|
||||
let uploadButton = (
|
||||
<WorkpadUpload onUpload={this.uploadWorkpad} disabled={createPending || !canUserWrite} />
|
||||
<EuiFilePicker
|
||||
compressed
|
||||
className="canvasWorkpad__upload--compressed"
|
||||
initialPromptText="Import workpad JSON file"
|
||||
onChange={([file]) => uploadWorkpad(file, this.onUpload)}
|
||||
accept="application/json"
|
||||
disabled={createPending || !canUserWrite}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!canUserWrite) {
|
||||
|
|
|
@ -1,24 +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 { EuiFilePicker } from '@elastic/eui';
|
||||
import { uploadWorkpad } from './upload_workpad';
|
||||
|
||||
export const WorkpadUpload = ({ onUpload, ...rest }) => (
|
||||
<EuiFilePicker
|
||||
{...rest}
|
||||
compressed
|
||||
className="canvasWorkpad__upload--compressed"
|
||||
initialPromptText="Import workpad JSON file"
|
||||
onChange={([file]) => uploadWorkpad(file, onUpload)}
|
||||
/>
|
||||
);
|
||||
|
||||
WorkpadUpload.propTypes = {
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
};
|
|
@ -4,4 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { FileUpload } from './file_upload';
|
||||
export const findExistingAsset = (type, content, assets) => {
|
||||
const existingId = Object.keys(assets).find(
|
||||
assetId => assets[assetId].type === type && assets[assetId].value === content
|
||||
);
|
||||
return existingId;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue