Removed file_upload component (#29007) (#29654)

[6.x] Feat: Bulk Upload Assets (#29007)
This commit is contained in:
Catherine Liu 2019-01-30 13:54:19 -07:00 committed by GitHub
parent b5d00bbbfe
commit 5323a57fb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 166 additions and 96 deletions

View file

@ -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,

View file

@ -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}

View file

@ -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'];

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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;
},
};
};

View file

@ -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,
};

View file

@ -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;
}

View file

@ -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 => ({

View file

@ -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,

View file

@ -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) {

View file

@ -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,
};

View file

@ -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;
};