Merge pull request #1309 from processing/feature/public-api-asset-limit
Feature/public api asset limit, Fixes #168
This commit is contained in:
commit
a6f59fd309
27 changed files with 512 additions and 146 deletions
|
@ -80,6 +80,8 @@ export const SHOW_NEW_FOLDER_MODAL = 'SHOW_NEW_FOLDER_MODAL';
|
|||
export const CLOSE_NEW_FOLDER_MODAL = 'CLOSE_NEW_FOLDER_MODAL';
|
||||
export const SHOW_FOLDER_CHILDREN = 'SHOW_FOLDER_CHILDREN';
|
||||
export const HIDE_FOLDER_CHILDREN = 'HIDE_FOLDER_CHILDREN';
|
||||
export const OPEN_UPLOAD_FILE_MODAL = 'OPEN_UPLOAD_FILE_MODAL';
|
||||
export const CLOSE_UPLOAD_FILE_MODAL = 'CLOSE_UPLOAD_FILE_MODAL';
|
||||
|
||||
export const SHOW_SHARE_MODAL = 'SHOW_SHARE_MODAL';
|
||||
export const CLOSE_SHARE_MODAL = 'CLOSE_SHARE_MODAL';
|
||||
|
@ -127,6 +129,7 @@ export const CLEAR_PERSISTED_STATE = 'CLEAR_PERSISTED_STATE';
|
|||
export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
|
||||
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
|
||||
export const SET_ASSETS = 'SET_ASSETS';
|
||||
export const DELETE_ASSET = 'DELETE_ASSET';
|
||||
|
||||
export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION';
|
||||
export const SET_SORTING = 'SET_SORTING';
|
||||
|
|
|
@ -30,8 +30,23 @@ export function getAssets() {
|
|||
};
|
||||
}
|
||||
|
||||
export function deleteAsset(assetKey, userId) {
|
||||
export function deleteAsset(assetKey) {
|
||||
return {
|
||||
type: 'PLACEHOLDER'
|
||||
type: ActionTypes.DELETE_ASSET,
|
||||
key: assetKey
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteAssetRequest(assetKey) {
|
||||
return (dispatch) => {
|
||||
axios.delete(`${ROOT_URL}/S3/${assetKey}`, { withCredentials: true })
|
||||
.then((response) => {
|
||||
dispatch(deleteAsset(assetKey));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -75,6 +75,19 @@ export function closeNewFileModal() {
|
|||
};
|
||||
}
|
||||
|
||||
export function openUploadFileModal(parentId) {
|
||||
return {
|
||||
type: ActionTypes.OPEN_UPLOAD_FILE_MODAL,
|
||||
parentId
|
||||
};
|
||||
}
|
||||
|
||||
export function closeUploadFileModal() {
|
||||
return {
|
||||
type: ActionTypes.CLOSE_UPLOAD_FILE_MODAL
|
||||
};
|
||||
}
|
||||
|
||||
export function expandSidebar() {
|
||||
return {
|
||||
type: ActionTypes.EXPAND_SIDEBAR
|
||||
|
|
|
@ -66,11 +66,11 @@ export function dropzoneAcceptCallback(userId, file, done) {
|
|||
done();
|
||||
})
|
||||
.catch((response) => {
|
||||
file.custom_status = 'rejected'; // eslint-disable-line
|
||||
if (response.data.responseText && response.data.responseText.message) {
|
||||
file.custom_status = 'rejected'; // eslint-disable-line
|
||||
if (response.data && response.data.responseText && response.data.responseText.message) {
|
||||
done(response.data.responseText.message);
|
||||
}
|
||||
done('error preparing the upload');
|
||||
done('Error: Reached upload limit.');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,9 +5,146 @@ import { bindActionCreators } from 'redux';
|
|||
import { Link } from 'react-router';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
|
||||
import Loader from '../../App/components/loader';
|
||||
import * as AssetActions from '../actions/assets';
|
||||
import downFilledTriangle from '../../../images/down-filled-triangle.svg';
|
||||
|
||||
class AssetListRowBase extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isFocused: false,
|
||||
optionsOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
onFocusComponent = () => {
|
||||
this.setState({ isFocused: true });
|
||||
}
|
||||
|
||||
onBlurComponent = () => {
|
||||
this.setState({ isFocused: false });
|
||||
setTimeout(() => {
|
||||
if (!this.state.isFocused) {
|
||||
this.closeOptions();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
openOptions = () => {
|
||||
this.setState({
|
||||
optionsOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
closeOptions = () => {
|
||||
this.setState({
|
||||
optionsOpen: false
|
||||
});
|
||||
}
|
||||
|
||||
toggleOptions = () => {
|
||||
if (this.state.optionsOpen) {
|
||||
this.closeOptions();
|
||||
} else {
|
||||
this.openOptions();
|
||||
}
|
||||
}
|
||||
|
||||
handleDropdownOpen = () => {
|
||||
this.closeOptions();
|
||||
this.openOptions();
|
||||
}
|
||||
|
||||
handleAssetDelete = () => {
|
||||
const { key, name } = this.props.asset;
|
||||
this.closeOptions();
|
||||
if (window.confirm(`Are you sure you want to delete "${name}"?`)) {
|
||||
this.props.deleteAssetRequest(key);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { asset, username } = this.props;
|
||||
const { optionsOpen } = this.state;
|
||||
return (
|
||||
<tr className="asset-table__row" key={asset.key}>
|
||||
<th scope="row">
|
||||
<Link to={asset.url} target="_blank">
|
||||
{asset.name}
|
||||
</Link>
|
||||
</th>
|
||||
<td>{prettyBytes(asset.size)}</td>
|
||||
<td>
|
||||
{ asset.sketchId && <Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link> }
|
||||
</td>
|
||||
<td className="asset-table__dropdown-column">
|
||||
<button
|
||||
className="asset-table__dropdown-button"
|
||||
onClick={this.toggleOptions}
|
||||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
>
|
||||
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
||||
</button>
|
||||
{optionsOpen &&
|
||||
<ul
|
||||
className="asset-table__action-dialogue"
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
className="asset-table__action-option"
|
||||
onClick={this.handleAssetDelete}
|
||||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={asset.url}
|
||||
target="_blank"
|
||||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
className="asset-table__action-option"
|
||||
>
|
||||
Open in New Tab
|
||||
</Link>
|
||||
</li>
|
||||
</ul>}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AssetListRowBase.propTypes = {
|
||||
asset: PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
sketchId: PropTypes.string,
|
||||
sketchName: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired
|
||||
}).isRequired,
|
||||
deleteAssetRequest: PropTypes.func.isRequired,
|
||||
username: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
function mapStateToPropsAssetListRow(state) {
|
||||
return {
|
||||
username: state.user.username
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToPropsAssetListRow(dispatch) {
|
||||
return bindActionCreators(AssetActions, dispatch);
|
||||
}
|
||||
|
||||
const AssetListRow = connect(mapStateToPropsAssetListRow, mapDispatchToPropsAssetListRow)(AssetListRowBase);
|
||||
|
||||
class AssetList extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -16,10 +153,7 @@ class AssetList extends React.Component {
|
|||
}
|
||||
|
||||
getAssetsTitle() {
|
||||
if (!this.props.username || this.props.username === this.props.user.username) {
|
||||
return 'p5.js Web Editor | My assets';
|
||||
}
|
||||
return `p5.js Web Editor | ${this.props.username}'s assets`;
|
||||
return 'p5.js Web Editor | My assets';
|
||||
}
|
||||
|
||||
hasAssets() {
|
||||
|
@ -39,7 +173,6 @@ class AssetList extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||
const { assetList } = this.props;
|
||||
return (
|
||||
<div className="asset-table-container">
|
||||
|
@ -52,24 +185,14 @@ class AssetList extends React.Component {
|
|||
<table className="asset-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Size</th>
|
||||
<th scope="col">Sketch</th>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>Sketch</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{assetList.map(asset =>
|
||||
(
|
||||
<tr className="asset-table__row" key={asset.key}>
|
||||
<th scope="row">
|
||||
<Link to={asset.url} target="_blank">
|
||||
{asset.name}
|
||||
</Link>
|
||||
</th>
|
||||
<td>{prettyBytes(asset.size)}</td>
|
||||
<td><Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td>
|
||||
</tr>
|
||||
))}
|
||||
{assetList.map(asset => <AssetListRow asset={asset} key={asset.key} />)}
|
||||
</tbody>
|
||||
</table>}
|
||||
</div>
|
||||
|
@ -81,7 +204,6 @@ AssetList.propTypes = {
|
|||
user: PropTypes.shape({
|
||||
username: PropTypes.string
|
||||
}).isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
assetList: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
|
@ -97,7 +219,6 @@ function mapStateToProps(state) {
|
|||
return {
|
||||
user: state.user,
|
||||
assetList: state.assets.list,
|
||||
totalSize: state.assets.totalSize,
|
||||
loading: state.loading
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,8 +3,9 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
const MB_TO_B = 1000 * 1000;
|
||||
const MAX_SIZE_B = 250 * MB_TO_B;
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
||||
const MAX_SIZE_B = limit;
|
||||
|
||||
const formatPercent = (percent) => {
|
||||
const percentUsed = percent * 100;
|
||||
|
@ -17,7 +18,7 @@ const formatPercent = (percent) => {
|
|||
|
||||
/* Eventually, this copy should be Total / 250 MB Used */
|
||||
const AssetSize = ({ totalSize }) => {
|
||||
if (!totalSize) {
|
||||
if (totalSize === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -25,9 +26,10 @@ const AssetSize = ({ totalSize }) => {
|
|||
const sizeLimit = prettyBytes(MAX_SIZE_B);
|
||||
const percentValue = totalSize / MAX_SIZE_B;
|
||||
const percent = formatPercent(percentValue);
|
||||
const percentSize = percentValue < 1 ? percentValue : 1;
|
||||
|
||||
return (
|
||||
<div className="asset-size" style={{ '--percent': percentValue }}>
|
||||
<div className="asset-size" style={{ '--percent': percentSize }}>
|
||||
<div className="asset-size-bar" />
|
||||
<p className="asset-current">{currentSize} ({percent})</p>
|
||||
<p className="asset-max">Max: {sizeLimit}</p>
|
||||
|
@ -42,7 +44,7 @@ AssetSize.propTypes = {
|
|||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
totalSize: state.assets.totalSize,
|
||||
totalSize: state.user.totalSize || state.assets.totalSize,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ class FileUploader extends React.Component {
|
|||
thumbnailWidth: 200,
|
||||
thumbnailHeight: 200,
|
||||
acceptedFiles: fileExtensionsAndMimeTypes,
|
||||
dictDefaultMessage: 'Drop files here to upload or click to use the file browser',
|
||||
dictDefaultMessage: 'Drop files here or click to use the file browser',
|
||||
accept: this.props.dropzoneAcceptCallback.bind(this, userId),
|
||||
sending: this.props.dropzoneSendingCallback,
|
||||
complete: this.props.dropzoneCompleteCallback
|
||||
|
|
|
@ -22,16 +22,18 @@ class NewFileForm extends React.Component {
|
|||
handleSubmit(this.createFile)(data);
|
||||
}}
|
||||
>
|
||||
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
|
||||
<input
|
||||
className="new-file-form__name-input"
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
{...domOnlyProps(name)}
|
||||
ref={(element) => { this.fileName = element; }}
|
||||
/>
|
||||
<input type="submit" value="Add File" aria-label="add file" />
|
||||
<div className="new-file-form__input-wrapper">
|
||||
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
|
||||
<input
|
||||
className="new-file-form__name-input"
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
{...domOnlyProps(name)}
|
||||
ref={(element) => { this.fileName = element; }}
|
||||
/>
|
||||
<input type="submit" value="Add File" aria-label="add file" />
|
||||
</div>
|
||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, compose } from 'redux';
|
||||
import { reduxForm } from 'redux-form';
|
||||
import classNames from 'classnames';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import NewFileForm from './NewFileForm';
|
||||
import FileUploader from './FileUploader';
|
||||
import { getCanUploadMedia, getreachedTotalSizeLimit } from '../selectors/users';
|
||||
import { closeNewFileModal } from '../actions/ide';
|
||||
import { createFile } from '../actions/files';
|
||||
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
||||
|
||||
const exitUrl = require('../../../images/exit.svg');
|
||||
|
@ -28,16 +31,12 @@ class NewFileModal extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const modalClass = classNames({
|
||||
'modal': true,
|
||||
'modal--reduced': !this.props.canUploadMedia
|
||||
});
|
||||
return (
|
||||
<section className={modalClass} ref={(element) => { this.modal = element; }}>
|
||||
<section className="modal" ref={(element) => { this.modal = element; }}>
|
||||
<div className="modal-content">
|
||||
<div className="modal__header">
|
||||
<h2 className="modal__title">Add File</h2>
|
||||
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
||||
<h2 className="modal__title">Create File</h2>
|
||||
<button className="modal__exit-button" onClick={this.props.closeNewFileModal}>
|
||||
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -45,17 +44,6 @@ class NewFileModal extends React.Component {
|
|||
focusOnModal={this.focusOnModal}
|
||||
{...this.props}
|
||||
/>
|
||||
{(() => {
|
||||
if (this.props.canUploadMedia) {
|
||||
return (
|
||||
<div>
|
||||
<p className="modal__divider">OR</p>
|
||||
<FileUploader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return '';
|
||||
})()}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
@ -63,8 +51,8 @@ class NewFileModal extends React.Component {
|
|||
}
|
||||
|
||||
NewFileModal.propTypes = {
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
canUploadMedia: PropTypes.bool.isRequired
|
||||
createFile: PropTypes.func.isRequired,
|
||||
closeNewFileModal: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
function validate(formProps) {
|
||||
|
@ -79,9 +67,22 @@ function validate(formProps) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
canUploadMedia: getCanUploadMedia(state),
|
||||
reachedTotalSizeLimit: getreachedTotalSizeLimit(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default reduxForm({
|
||||
form: 'new-file',
|
||||
fields: ['name'],
|
||||
validate
|
||||
})(NewFileModal);
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators({ createFile, closeNewFileModal }, dispatch);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
reduxForm({
|
||||
form: 'new-file',
|
||||
fields: ['name'],
|
||||
validate
|
||||
})
|
||||
)(NewFileModal);
|
||||
|
|
|
@ -25,16 +25,18 @@ class NewFolderForm extends React.Component {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<label className="new-folder-form__name-label" htmlFor="name">Name:</label>
|
||||
<input
|
||||
className="new-folder-form__name-input"
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
ref={(element) => { this.fileName = element; }}
|
||||
{...domOnlyProps(name)}
|
||||
/>
|
||||
<input type="submit" value="Add Folder" aria-label="add folder" />
|
||||
<div className="new-folder-form__input-wrapper">
|
||||
<label className="new-folder-form__name-label" htmlFor="name">Name:</label>
|
||||
<input
|
||||
className="new-folder-form__name-input"
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
ref={(element) => { this.fileName = element; }}
|
||||
{...domOnlyProps(name)}
|
||||
/>
|
||||
<input type="submit" value="Add Folder" aria-label="add folder" />
|
||||
</div>
|
||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -16,7 +16,7 @@ class NewFolderModal extends React.Component {
|
|||
<section className="modal" ref={(element) => { this.newFolderModal = element; }} >
|
||||
<div className="modal-content-folder">
|
||||
<div className="modal__header">
|
||||
<h2 className="modal__title">Add Folder</h2>
|
||||
<h2 className="modal__title">Create Folder</h2>
|
||||
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
||||
<InlineSVG src={exitUrl} alt="Close New Folder Modal" />
|
||||
</button>
|
||||
|
|
|
@ -97,7 +97,7 @@ class Sidebar extends React.Component {
|
|||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
>
|
||||
Add folder
|
||||
Create folder
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -110,7 +110,20 @@ class Sidebar extends React.Component {
|
|||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
>
|
||||
Add file
|
||||
Create file
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
aria-label="upload file"
|
||||
onClick={() => {
|
||||
this.props.openUploadFileModal(rootFile.id);
|
||||
setTimeout(this.props.closeProjectOptions, 0);
|
||||
}}
|
||||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
>
|
||||
Upload file
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -137,6 +150,7 @@ Sidebar.propTypes = {
|
|||
openProjectOptions: PropTypes.func.isRequired,
|
||||
closeProjectOptions: PropTypes.func.isRequired,
|
||||
newFolder: PropTypes.func.isRequired,
|
||||
openUploadFileModal: PropTypes.func.isRequired,
|
||||
owner: PropTypes.shape({
|
||||
id: PropTypes.string
|
||||
}),
|
||||
|
|
68
client/modules/IDE/components/UploadFileModal.jsx
Normal file
68
client/modules/IDE/components/UploadFileModal.jsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import FileUploader from './FileUploader';
|
||||
import { getreachedTotalSizeLimit } from '../selectors/users';
|
||||
import exitUrl from '../../../images/exit.svg';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
||||
const limitText = prettyBytes(limit);
|
||||
|
||||
class UploadFileModal extends React.Component {
|
||||
propTypes = {
|
||||
reachedTotalSizeLimit: PropTypes.bool.isRequired,
|
||||
closeModal: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.focusOnModal();
|
||||
}
|
||||
|
||||
focusOnModal = () => {
|
||||
this.modal.focus();
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section className="modal" ref={(element) => { this.modal = element; }}>
|
||||
<div className="modal-content">
|
||||
<div className="modal__header">
|
||||
<h2 className="modal__title">Upload File</h2>
|
||||
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
||||
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
||||
</button>
|
||||
</div>
|
||||
{ this.props.reachedTotalSizeLimit &&
|
||||
<p>
|
||||
{
|
||||
`Error: You cannot upload any more files. You have reached the total size limit of ${limitText}.
|
||||
If you would like to upload more, please remove the ones you aren't using anymore by
|
||||
in your `
|
||||
}
|
||||
<Link to="/assets" onClick={this.props.closeModal}>assets</Link>
|
||||
.
|
||||
</p>
|
||||
}
|
||||
{ !this.props.reachedTotalSizeLimit &&
|
||||
<div>
|
||||
<FileUploader />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
reachedTotalSizeLimit: getreachedTotalSizeLimit(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(UploadFileModal);
|
|
@ -12,6 +12,7 @@ import Toolbar from '../components/Toolbar';
|
|||
import Preferences from '../components/Preferences';
|
||||
import NewFileModal from '../components/NewFileModal';
|
||||
import NewFolderModal from '../components/NewFolderModal';
|
||||
import UploadFileModal from '../components/UploadFileModal';
|
||||
import ShareModal from '../components/ShareModal';
|
||||
import KeyboardShortcutModal from '../components/KeyboardShortcutModal';
|
||||
import ErrorModal from '../components/ErrorModal';
|
||||
|
@ -238,6 +239,8 @@ class IDEView extends React.Component {
|
|||
newFolder={this.props.newFolder}
|
||||
user={this.props.user}
|
||||
owner={this.props.project.owner}
|
||||
openUploadFileModal={this.props.openUploadFileModal}
|
||||
closeUploadFileModal={this.props.closeUploadFileModal}
|
||||
/>
|
||||
<SplitPane
|
||||
split="vertical"
|
||||
|
@ -349,12 +352,8 @@ class IDEView extends React.Component {
|
|||
</SplitPane>
|
||||
</SplitPane>
|
||||
</div>
|
||||
{this.props.ide.modalIsVisible &&
|
||||
<NewFileModal
|
||||
canUploadMedia={this.props.user.authenticated}
|
||||
closeModal={this.props.closeNewFileModal}
|
||||
createFile={this.props.createFile}
|
||||
/>
|
||||
{ this.props.ide.modalIsVisible &&
|
||||
<NewFileModal />
|
||||
}
|
||||
{this.props.ide.newFolderModalVisible &&
|
||||
<NewFolderModal
|
||||
|
@ -362,6 +361,11 @@ class IDEView extends React.Component {
|
|||
createFolder={this.props.createFolder}
|
||||
/>
|
||||
}
|
||||
{this.props.ide.uploadFileModalVisible &&
|
||||
<UploadFileModal
|
||||
closeModal={this.props.closeUploadFileModal}
|
||||
/>
|
||||
}
|
||||
{ this.props.location.pathname === '/about' &&
|
||||
<Overlay
|
||||
title="About"
|
||||
|
@ -475,6 +479,7 @@ IDEView.propTypes = {
|
|||
justOpenedProject: PropTypes.bool.isRequired,
|
||||
errorType: PropTypes.string,
|
||||
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||
uploadFileModalVisible: PropTypes.bool.isRequired
|
||||
}).isRequired,
|
||||
stopSketch: PropTypes.func.isRequired,
|
||||
project: PropTypes.shape({
|
||||
|
@ -532,7 +537,6 @@ IDEView.propTypes = {
|
|||
}).isRequired,
|
||||
dispatchConsoleEvent: PropTypes.func.isRequired,
|
||||
newFile: PropTypes.func.isRequired,
|
||||
closeNewFileModal: PropTypes.func.isRequired,
|
||||
expandSidebar: PropTypes.func.isRequired,
|
||||
collapseSidebar: PropTypes.func.isRequired,
|
||||
cloneProject: PropTypes.func.isRequired,
|
||||
|
@ -545,7 +549,6 @@ IDEView.propTypes = {
|
|||
newFolder: PropTypes.func.isRequired,
|
||||
closeNewFolderModal: PropTypes.func.isRequired,
|
||||
createFolder: PropTypes.func.isRequired,
|
||||
createFile: PropTypes.func.isRequired,
|
||||
closeShareModal: PropTypes.func.isRequired,
|
||||
showEditorOptions: PropTypes.func.isRequired,
|
||||
closeEditorOptions: PropTypes.func.isRequired,
|
||||
|
@ -577,6 +580,8 @@ IDEView.propTypes = {
|
|||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
startSketch: PropTypes.func.isRequired,
|
||||
openUploadFileModal: PropTypes.func.isRequired,
|
||||
closeUploadFileModal: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
|
|
|
@ -10,6 +10,8 @@ const assets = (state = initialState, action) => {
|
|||
switch (action.type) {
|
||||
case ActionTypes.SET_ASSETS:
|
||||
return { list: action.assets, totalSize: action.totalSize };
|
||||
case ActionTypes.DELETE_ASSET:
|
||||
return { list: state.list.filter(asset => asset.key !== action.key) };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ const initialState = {
|
|||
preferencesIsVisible: false,
|
||||
projectOptionsVisible: false,
|
||||
newFolderModalVisible: false,
|
||||
uploadFileModalVisible: false,
|
||||
shareModalVisible: false,
|
||||
shareModalProjectId: 'abcd',
|
||||
shareModalProjectName: 'My Cute Sketch',
|
||||
|
@ -105,6 +106,10 @@ const ide = (state = initialState, action) => {
|
|||
return Object.assign({}, state, { runtimeErrorWarningVisible: false });
|
||||
case ActionTypes.SHOW_RUNTIME_ERROR_WARNING:
|
||||
return Object.assign({}, state, { runtimeErrorWarningVisible: true });
|
||||
case ActionTypes.OPEN_UPLOAD_FILE_MODAL:
|
||||
return Object.assign({}, state, { uploadFileModalVisible: true, parentId: action.parentId });
|
||||
case ActionTypes.CLOSE_UPLOAD_FILE_MODAL:
|
||||
return Object.assign({}, state, { uploadFileModalVisible: false });
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
27
client/modules/IDE/selectors/users.js
Normal file
27
client/modules/IDE/selectors/users.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const getAuthenticated = state => state.user.authenticated;
|
||||
const getTotalSize = state => state.user.totalSize;
|
||||
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
||||
|
||||
export const getCanUploadMedia = createSelector(
|
||||
getAuthenticated,
|
||||
getTotalSize,
|
||||
(authenticated, totalSize) => {
|
||||
if (!authenticated) return false;
|
||||
// eventually do the same thing for verified when
|
||||
// email verification actually works
|
||||
if (totalSize > limit) return false;
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
export const getreachedTotalSizeLimit = createSelector(
|
||||
getTotalSize,
|
||||
(totalSize) => {
|
||||
if (totalSize > limit) return true;
|
||||
// if (totalSize > 1000) return true;
|
||||
return false;
|
||||
}
|
||||
);
|
|
@ -9,7 +9,8 @@
|
|||
|
||||
max-height: 100%;
|
||||
border-spacing: 0;
|
||||
& .sketch-list__dropdown-column {
|
||||
position: relative;
|
||||
& .asset-table__dropdown-column {
|
||||
width: #{60 / $base-font-size}rem;
|
||||
position: relative;
|
||||
}
|
||||
|
@ -66,3 +67,33 @@
|
|||
font-size: #{16 / $base-font-size}rem;
|
||||
padding: #{42 / $base-font-size}rem 0;
|
||||
}
|
||||
|
||||
.asset-table__total {
|
||||
padding: 0 #{20 / $base-font-size}rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@include themify() {
|
||||
background-color: getThemifyVariable('background-color');
|
||||
}
|
||||
}
|
||||
|
||||
.asset-table__dropdown-button {
|
||||
width:#{25 / $base-font-size}rem;
|
||||
height:#{25 / $base-font-size}rem;
|
||||
|
||||
@include themify() {
|
||||
& polygon {
|
||||
fill: getThemifyVariable('dropdown-color');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.asset-table__action-dialogue {
|
||||
@extend %dropdown-open-right;
|
||||
top: 63%;
|
||||
right: calc(100% - 26px);
|
||||
}
|
||||
|
||||
.asset-table__action-option {
|
||||
font-size: #{12 / $base-font-size}rem;
|
||||
}
|
||||
|
|
|
@ -37,15 +37,12 @@
|
|||
|
||||
.asset-current {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: calc(200px * var(--percent));
|
||||
margin-left: -8px;
|
||||
top: #{28 / $base-font-size}rem;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.asset-max {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(210%); // align max label to right of asset-size-bar
|
||||
padding-left: #{8 / $base-font-size}rem;
|
||||
left: #{210 / $base-font-size}rem;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
.modal-content {
|
||||
@extend %modal;
|
||||
min-height: #{150 / $base-font-size}rem;
|
||||
width: #{700 / $base-font-size}rem;
|
||||
width: #{500 / $base-font-size}rem;
|
||||
padding: #{20 / $base-font-size}rem;
|
||||
.modal--reduced & {
|
||||
//min-height: #{150 / $base-font-size}rem;
|
||||
|
@ -32,9 +32,8 @@
|
|||
margin-bottom: #{20 / $base-font-size}rem;
|
||||
}
|
||||
|
||||
.new-file-form, .new-file-folder {
|
||||
.new-folder-form__input-wrapper, .new-file-form__input-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.new-file-form__name-label, .new-folder-form__name-label {
|
||||
|
@ -43,6 +42,7 @@
|
|||
|
||||
.new-file-form__name-input, .new-folder-form__name-input {
|
||||
margin-right: #{10 / $base-font-size}rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal__divider {
|
||||
|
|
|
@ -60,13 +60,24 @@ export function deleteObjectsFromS3(keyList, callback) {
|
|||
}
|
||||
|
||||
export function deleteObjectFromS3(req, res) {
|
||||
const objectKey = req.params.object_key;
|
||||
deleteObjectsFromS3([objectKey], () => {
|
||||
const { objectKey, userId } = req.params;
|
||||
let fullObjectKey;
|
||||
if (userId) {
|
||||
fullObjectKey = `${userId}/${objectKey}`;
|
||||
} else {
|
||||
fullObjectKey = objectKey;
|
||||
}
|
||||
deleteObjectsFromS3([fullObjectKey], () => {
|
||||
res.json({ success: true });
|
||||
});
|
||||
}
|
||||
|
||||
export function signS3(req, res) {
|
||||
const limit = process.env.UPLOAD_LIMIT || 250000000;
|
||||
if (req.user.totalSize > limit) {
|
||||
res.status(403).send({ message: 'user has uploaded the maximum size of assets.' });
|
||||
return;
|
||||
}
|
||||
const fileExtension = getExtension(req.body.name);
|
||||
const filename = uuid.v4() + fileExtension;
|
||||
const acl = 'public-read';
|
||||
|
@ -84,7 +95,7 @@ export function signS3(req, res) {
|
|||
policy: p.policy,
|
||||
signature: p.signature
|
||||
};
|
||||
return res.json(result);
|
||||
res.json(result);
|
||||
}
|
||||
|
||||
export function copyObjectInS3(req, res) {
|
||||
|
@ -108,54 +119,65 @@ export function copyObjectInS3(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
export function listObjectsInS3ForUser(req, res) {
|
||||
const { username } = req.user;
|
||||
findUserByUsername(username, (user) => {
|
||||
const userId = user.id;
|
||||
export function listObjectsInS3ForUser(userId) {
|
||||
let assets = [];
|
||||
return new Promise((resolve) => {
|
||||
const params = {
|
||||
s3Params: {
|
||||
Bucket: `${process.env.S3_BUCKET}`,
|
||||
Prefix: `${userId}/`
|
||||
}
|
||||
};
|
||||
let assets = [];
|
||||
client.listObjects(params)
|
||||
.on('data', (data) => {
|
||||
assets = assets.concat(data.Contents.map(object => ({ key: object.Key, size: object.Size })));
|
||||
})
|
||||
.on('end', () => {
|
||||
const projectAssets = [];
|
||||
getProjectsForUserId(userId).then((projects) => {
|
||||
let totalSize = 0;
|
||||
assets.forEach((asset) => {
|
||||
const name = asset.key.split('/').pop();
|
||||
const foundAsset = {
|
||||
key: asset.key,
|
||||
name,
|
||||
size: asset.size,
|
||||
url: `${process.env.S3_BUCKET_URL_BASE}${asset.key}`
|
||||
};
|
||||
totalSize += asset.size;
|
||||
projects.some((project) => {
|
||||
let found = false;
|
||||
project.files.some((file) => {
|
||||
if (!file.url) return false;
|
||||
if (file.url.includes(asset.key)) {
|
||||
found = true;
|
||||
foundAsset.name = file.name;
|
||||
foundAsset.sketchName = project.name;
|
||||
foundAsset.sketchId = project.id;
|
||||
foundAsset.url = file.url;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return found;
|
||||
});
|
||||
projectAssets.push(foundAsset);
|
||||
});
|
||||
res.json({ assets: projectAssets, totalSize });
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
}).then(() => getProjectsForUserId(userId)).then((projects) => {
|
||||
const projectAssets = [];
|
||||
let totalSize = 0;
|
||||
assets.forEach((asset) => {
|
||||
const name = asset.key.split('/').pop();
|
||||
const foundAsset = {
|
||||
key: asset.key,
|
||||
name,
|
||||
size: asset.size,
|
||||
url: `${process.env.S3_BUCKET_URL_BASE}${asset.key}`
|
||||
};
|
||||
totalSize += asset.size;
|
||||
projects.some((project) => {
|
||||
let found = false;
|
||||
project.files.some((file) => {
|
||||
if (!file.url) return false;
|
||||
if (file.url.includes(asset.key)) {
|
||||
found = true;
|
||||
foundAsset.name = file.name;
|
||||
foundAsset.sketchName = project.name;
|
||||
foundAsset.sketchId = project.id;
|
||||
foundAsset.url = file.url;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return found;
|
||||
});
|
||||
projectAssets.push(foundAsset);
|
||||
});
|
||||
return Promise.resolve({ assets: projectAssets, totalSize });
|
||||
}).catch((err) => {
|
||||
console.log('got an error');
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
export function listObjectsInS3ForUserRequestHandler(req, res) {
|
||||
const { username } = req.user;
|
||||
findUserByUsername(username, (user) => {
|
||||
const userId = user.id;
|
||||
listObjectsInS3ForUser(userId).then((objects) => {
|
||||
res.json(objects);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@ export function userResponse(user) {
|
|||
preferences: user.preferences,
|
||||
apiKeys: user.apiKeys,
|
||||
verified: user.verified,
|
||||
id: user._id
|
||||
id: user._id,
|
||||
totalSize: user.totalSize
|
||||
};
|
||||
}
|
||||
|
||||
|
|
29
server/migrations/populateTotalSize.js
Normal file
29
server/migrations/populateTotalSize.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
/* eslint-disable */
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import User from '../models/user';
|
||||
import { listObjectsInS3ForUser } from '../controllers/aws.controller';
|
||||
|
||||
// Connect to MongoDB
|
||||
mongoose.Promise = global.Promise;
|
||||
mongoose.connect(process.env.MONGO_URL, { useMongoClient: true });
|
||||
mongoose.connection.on('error', () => {
|
||||
console.error('MongoDB Connection Error. Please make sure that MongoDB is running.');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
User.find({}, {}, { timeout: true }).cursor().eachAsync((user) => {
|
||||
console.log(user.id);
|
||||
if (user.totalSize !== undefined) {
|
||||
console.log('Already updated size for user: ' + user.username);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return listObjectsInS3ForUser(user.id).then((objects) => {
|
||||
return User.findByIdAndUpdate(user.id, { $set: { totalSize: objects.totalSize } });
|
||||
}).then(() => {
|
||||
console.log('Updated new total size for user: ' + user.username);
|
||||
});
|
||||
}).then(() => {
|
||||
console.log('Done iterating over every user');
|
||||
process.exit(0);
|
||||
});
|
|
@ -1,3 +1,7 @@
|
|||
require('@babel/register');
|
||||
require('@babel/polyfill');
|
||||
require('./truncate');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.resolve('.env') });
|
||||
require('./populateTotalSize');
|
||||
// require('./moveBucket');
|
||||
// require('./truncate');
|
||||
|
|
|
@ -66,7 +66,8 @@ const userSchema = new Schema({
|
|||
soundOutput: { type: Boolean, default: false },
|
||||
theme: { type: String, default: 'light' },
|
||||
autorefresh: { type: Boolean, default: false }
|
||||
}
|
||||
},
|
||||
totalSize: { type: Number, default: 0 }
|
||||
}, { timestamps: true, usePushEach: true });
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,7 @@ const router = new Router();
|
|||
|
||||
router.post('/S3/sign', isAuthenticated, AWSController.signS3);
|
||||
router.post('/S3/copy', isAuthenticated, AWSController.copyObjectInS3);
|
||||
router.delete('/S3/:object_key', isAuthenticated, AWSController.deleteObjectFromS3);
|
||||
router.get('/S3/objects', AWSController.listObjectsInS3ForUser);
|
||||
router.delete('/S3/:userId?/:objectKey', isAuthenticated, AWSController.deleteObjectFromS3);
|
||||
router.get('/S3/objects', AWSController.listObjectsInS3ForUserRequestHandler);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -31,6 +31,7 @@ export function renderIndex() {
|
|||
window.process.env.EXAMPLES_ENABLED = ${process.env.EXAMPLES_ENABLED === 'false' ? false : true};
|
||||
window.process.env.UI_ACCESS_TOKEN_ENABLED = ${process.env.UI_ACCESS_TOKEN_ENABLED === 'false' ? false : true};
|
||||
window.process.env.UI_COLLECTIONS_ENABLED = ${process.env.UI_COLLECTIONS_ENABLED === 'false' ? false : true};
|
||||
window.process.env.UPLOAD_LIMIT = ${process.env.UPLOAD_LIMIT ? `${process.env.UPLOAD_LIMIT}` : undefined};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
Loading…
Reference in a new issue