From 36d20281dbfc40b0422e2f87161a4ac38f759c16 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Thu, 8 Aug 2019 17:34:49 -0400 Subject: [PATCH 1/8] add script to add total size to all user accounts, add totalSize to user model, return totalSize from user api request --- server/controllers/aws.controller.js | 87 +++++++++++++++----------- server/controllers/user.controller.js | 3 +- server/migrations/populateTotalSize.js | 28 +++++++++ server/migrations/start.js | 6 +- server/models/user.js | 3 +- server/routes/aws.routes.js | 2 +- 6 files changed, 88 insertions(+), 41 deletions(-) create mode 100644 server/migrations/populateTotalSize.js diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js index 3cdd7dfc..e8744959 100644 --- a/server/controllers/aws.controller.js +++ b/server/controllers/aws.controller.js @@ -108,54 +108,67 @@ 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(() => { + return 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); + }); }); } diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 9c0d5076..3ffdb026 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -17,7 +17,8 @@ export function userResponse(user) { preferences: user.preferences, apiKeys: user.apiKeys, verified: user.verified, - id: user._id + id: user._id, + totalSize: req.user.totalSize }; } diff --git a/server/migrations/populateTotalSize.js b/server/migrations/populateTotalSize.js new file mode 100644 index 00000000..6551eceb --- /dev/null +++ b/server/migrations/populateTotalSize.js @@ -0,0 +1,28 @@ +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); +}); \ No newline at end of file diff --git a/server/migrations/start.js b/server/migrations/start.js index 0c68e0eb..a3127620 100644 --- a/server/migrations/start.js +++ b/server/migrations/start.js @@ -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'); diff --git a/server/models/user.js b/server/models/user.js index 0a1a4dc3..98c0e1fd 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -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 }); /** diff --git a/server/routes/aws.routes.js b/server/routes/aws.routes.js index 0bda0f8b..f73e4fa4 100644 --- a/server/routes/aws.routes.js +++ b/server/routes/aws.routes.js @@ -7,6 +7,6 @@ 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.get('/S3/objects', AWSController.listObjectsInS3ForUserRequestHandler); export default router; From b8fb51d28342f184fcb5583f466659acfd948ff3 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 12 Aug 2019 11:21:42 -0400 Subject: [PATCH 2/8] Fix merge conflict for cherry-picking 0b8e78df3a0c49d5dcf706b1c26346da3dcc0bc4 --- client/constants.js | 2 + client/modules/IDE/actions/ide.js | 12 ++++ .../modules/IDE/components/NewFileModal.jsx | 55 ++++++++++--------- client/modules/IDE/components/Sidebar.jsx | 14 +++++ .../IDE/components/UploadFileModal.jsx | 43 +++++++++++++++ client/modules/IDE/pages/IDEView.jsx | 18 +++--- client/modules/IDE/reducers/ide.js | 5 ++ client/modules/IDE/selectors/users.js | 24 ++++++++ 8 files changed, 138 insertions(+), 35 deletions(-) create mode 100644 client/modules/IDE/components/UploadFileModal.jsx create mode 100644 client/modules/IDE/selectors/users.js diff --git a/client/constants.js b/client/constants.js index b5d477b2..7d1a04dc 100644 --- a/client/constants.js +++ b/client/constants.js @@ -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'; diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index cf573573..8b453436 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -75,6 +75,18 @@ export function closeNewFileModal() { }; } +export function openUploadFileModal() { + return { + type: ActionTypes.OPEN_UPLOAD_FILE_MODAL + }; +} + +export function closeUploadFileModal() { + return { + type: ActionTypes.CLOSE_UPLOAD_FILE_MODAL + }; +} + export function expandSidebar() { return { type: ActionTypes.EXPAND_SIDEBAR diff --git a/client/modules/IDE/components/NewFileModal.jsx b/client/modules/IDE/components/NewFileModal.jsx index 433c3daa..e7747d2d 100644 --- a/client/modules/IDE/components/NewFileModal.jsx +++ b/client/modules/IDE/components/NewFileModal.jsx @@ -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 ( -
{ this.modal = element; }}> +
{ this.modal = element; }}>
-

Add File

-
@@ -45,17 +44,6 @@ class NewFileModal extends React.Component { focusOnModal={this.focusOnModal} {...this.props} /> - {(() => { - if (this.props.canUploadMedia) { - return ( -
-

OR

- -
- ); - } - return ''; - })()}
); @@ -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); diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index 2ea492c2..739c7867 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -113,6 +113,19 @@ class Sidebar extends React.Component { Add file +
  • + +
  • @@ -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 }), diff --git a/client/modules/IDE/components/UploadFileModal.jsx b/client/modules/IDE/components/UploadFileModal.jsx new file mode 100644 index 00000000..35e2fd9e --- /dev/null +++ b/client/modules/IDE/components/UploadFileModal.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import FileUploader from './FileUploader'; +import { getreachedTotalSizeLimit } from '../selectors/users'; + +class UploadFileModal extends React.Component { + propTypes = { + reachedTotalSizeLimit: PropTypes.bool.isRequired + } + + render() { + return ( +
    { this.modal = element; }}> + { this.props.reachedTotalSizeLimit && +

    + { + `You have reached the size limit for the number of files you can upload to your account. + If you would like to upload more, please remove the ones you aren't using anymore by + looking through your ` + } + assets + {'.'} +

    + } + { !this.props.reachedTotalSizeLimit && +
    + +
    + } +
    + ); + } +} + +function mapStateToProps(state) { + return { + reachedTotalSizeLimit: getreachedTotalSizeLimit(state) + }; +} + +export default connect(mapStateToProps)(UploadFileModal); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index b573d223..8b589e51 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -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'; @@ -349,12 +350,8 @@ class IDEView extends React.Component { - {this.props.ide.modalIsVisible && - + { this.props.ide.modalIsVisible && + } {this.props.ide.newFolderModalVisible && } + {this.props.ide.uploadFileModalVisible && + + } { this.props.location.pathname === '/about' && { 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 }); + case ActionTypes.CLOSE_UPLOAD_FILE_MODAL: + return Object.assign({}, state, { uploadFileModalVisible: false }); default: return state; } diff --git a/client/modules/IDE/selectors/users.js b/client/modules/IDE/selectors/users.js new file mode 100644 index 00000000..d223e4d9 --- /dev/null +++ b/client/modules/IDE/selectors/users.js @@ -0,0 +1,24 @@ +import { createSelector } from 'reselect'; + +const getAuthenticated = state => state.user.authenticated; +const getTotalSize = state => state.user.totalSize; + +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 > 250000000) return false; + return true; + } +); + +export const getreachedTotalSizeLimit = createSelector( + getTotalSize, + (totalSize) => { + if (totalSize > 250000000) return true; + return false; + } +); From 7c1aa2e589a31fe0b1ad61ffc155390021b024ff Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 10 Sep 2019 18:42:23 -0400 Subject: [PATCH 3/8] Fix merge conflicts for cherry picking 0bac332a9eb360978e686d6be8bff92f0fa1740c --- client/constants.js | 1 + client/modules/IDE/actions/assets.js | 19 +- client/modules/IDE/components/AssetList.jsx | 175 +++++++++++++++--- client/modules/IDE/components/Sidebar.jsx | 4 +- .../IDE/components/UploadFileModal.jsx | 53 ++++-- client/modules/IDE/pages/IDEView.jsx | 3 + client/modules/IDE/reducers/assets.js | 7 +- client/modules/IDE/selectors/users.js | 3 +- client/styles/components/_asset-list.scss | 29 ++- 9 files changed, 246 insertions(+), 48 deletions(-) diff --git a/client/constants.js b/client/constants.js index 7d1a04dc..477409fc 100644 --- a/client/constants.js +++ b/client/constants.js @@ -129,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'; diff --git a/client/modules/IDE/actions/assets.js b/client/modules/IDE/actions/assets.js index e2b49cf6..483e6d4e 100644 --- a/client/modules/IDE/actions/assets.js +++ b/client/modules/IDE/actions/assets.js @@ -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 + }); + }); }; } diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index 8c5d0826..491dc2b5 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -5,9 +5,144 @@ 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.closeAll(); + } + }, 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 ( + + + + {asset.name} + + + {prettyBytes(asset.size)} + + { asset.sketchId && {asset.sketchName} } + + + + {optionsOpen && +
      +
    • + +
    • +
    • + + Open in New Tab + +
    • +
    } + + + ); + } +} + +AssetListRowBase.propTypes = { + asset: PropTypes.shape({ + key: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + sketchId: PropTypes.string, + sketchName: PropTypes.string, + name: PropTypes.string.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 +151,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,10 +171,13 @@ class AssetList extends React.Component { } render() { - const username = this.props.username !== undefined ? this.props.username : this.props.user.username; - const { assetList } = this.props; + const { assetList, totalSize } = this.props; return (
    + {/* Eventually, this copy should be Total / 250 MB Used */} + {this.hasAssets() && totalSize && +

    {`${prettyBytes(totalSize)} Total`}

    + } {this.getAssetsTitle()} @@ -52,24 +187,14 @@ class AssetList extends React.Component { - - - + + + + - {assetList.map(asset => - ( - - - - - - ))} + {assetList.map(asset => )}
    NameSizeSketchNameSizeSketch
    - - {asset.name} - - {prettyBytes(asset.size)}{asset.sketchName}
    }
    @@ -81,7 +206,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, @@ -89,15 +213,20 @@ AssetList.propTypes = { sketchName: PropTypes.string, sketchId: PropTypes.string })).isRequired, + totalSize: PropTypes.number, getAssets: PropTypes.func.isRequired, loading: PropTypes.bool.isRequired }; +AssetList.defaultProps = { + totalSize: undefined +}; + function mapStateToProps(state) { return { user: state.user, assetList: state.assets.list, - totalSize: state.assets.totalSize, + totalSize: state.user.totalSize, loading: state.loading }; } diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index 739c7867..f6a5bdd1 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -110,7 +110,7 @@ class Sidebar extends React.Component { onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} > - Add file + Create file
  • @@ -123,7 +123,7 @@ class Sidebar extends React.Component { onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} > - Add file + Upload file
  • diff --git a/client/modules/IDE/components/UploadFileModal.jsx b/client/modules/IDE/components/UploadFileModal.jsx index 35e2fd9e..3e335fc0 100644 --- a/client/modules/IDE/components/UploadFileModal.jsx +++ b/client/modules/IDE/components/UploadFileModal.jsx @@ -2,33 +2,54 @@ 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 FileUploader from './FileUploader'; import { getreachedTotalSizeLimit } from '../selectors/users'; +import exitUrl from '../../../images/exit.svg'; + class UploadFileModal extends React.Component { propTypes = { - reachedTotalSizeLimit: PropTypes.bool.isRequired + reachedTotalSizeLimit: PropTypes.bool.isRequired, + closeModal: PropTypes.func.isRequired } + componentDidMount() { + this.focusOnModal(); + } + + focusOnModal = () => { + this.modal.focus(); + } + + render() { return (
    { this.modal = element; }}> - { this.props.reachedTotalSizeLimit && -

    - { - `You have reached the size limit for the number of files you can upload to your account. - If you would like to upload more, please remove the ones you aren't using anymore by - looking through your ` - } - assets - {'.'} -

    - } - { !this.props.reachedTotalSizeLimit && -
    - +
    +
    +

    Upload File

    +
    - } + { this.props.reachedTotalSizeLimit && +

    + { + `You have reached the size limit for the number of files you can upload to your account. + If you would like to upload more, please remove the ones you aren't using anymore by + looking through your ` + } + assets + {'.'} +

    + } + { !this.props.reachedTotalSizeLimit && +
    + +
    + } +
    ); } diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 8b589e51..f7179a14 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -239,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} /> { switch (action.type) { case ActionTypes.SET_ASSETS: - return { list: action.assets, totalSize: action.totalSize }; + return { list: action.assets }; + case ActionTypes.DELETE_ASSET: + return { list: state.list.filter(asset => asset.key !== action.key) }; default: return state; } diff --git a/client/modules/IDE/selectors/users.js b/client/modules/IDE/selectors/users.js index d223e4d9..d597c894 100644 --- a/client/modules/IDE/selectors/users.js +++ b/client/modules/IDE/selectors/users.js @@ -18,7 +18,8 @@ export const getCanUploadMedia = createSelector( export const getreachedTotalSizeLimit = createSelector( getTotalSize, (totalSize) => { - if (totalSize > 250000000) return true; + // if (totalSize > 250000000) return true; + if (totalSize > 1000) return true; return false; } ); diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss index 6d4e30b9..7cee736e 100644 --- a/client/styles/components/_asset-list.scss +++ b/client/styles/components/_asset-list.scss @@ -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,29 @@ 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); +} From 0cde29e56d7425889efbd7cd6f9100d8e744a720 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 25 Sep 2019 17:54:46 -0400 Subject: [PATCH 4/8] re #168, get asset deletion route to work, update options dropdown on asset list --- client/modules/IDE/components/AssetList.jsx | 3 ++- client/styles/components/_asset-list.scss | 4 ++++ server/controllers/aws.controller.js | 8 +++++++- server/controllers/user.controller.js | 2 +- server/routes/aws.routes.js | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index 491dc2b5..58d3909c 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -28,7 +28,7 @@ class AssetListRowBase extends React.Component { this.setState({ isFocused: false }); setTimeout(() => { if (!this.state.isFocused) { - this.closeAll(); + this.closeOptions(); } }, 200); } @@ -109,6 +109,7 @@ class AssetListRowBase extends React.Component { target="_blank" onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} + className="asset-table__action-option" > Open in New Tab diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss index 7cee736e..0a34f343 100644 --- a/client/styles/components/_asset-list.scss +++ b/client/styles/components/_asset-list.scss @@ -93,3 +93,7 @@ top: 63%; right: calc(100% - 26px); } + +.asset-table__action-option { + font-size: #{12 / $base-font-size}rem; +} diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js index e8744959..b4976a50 100644 --- a/server/controllers/aws.controller.js +++ b/server/controllers/aws.controller.js @@ -60,7 +60,13 @@ export function deleteObjectsFromS3(keyList, callback) { } export function deleteObjectFromS3(req, res) { - const objectKey = req.params.object_key; + const { object_key, user_id } = req.params; + let objectKey; + if (user_id) { + objectKey = `${user_id}/${object_key}` + } else { + objectKey = object_key; + } deleteObjectsFromS3([objectKey], () => { res.json({ success: true }); }); diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 3ffdb026..d326e373 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -18,7 +18,7 @@ export function userResponse(user) { apiKeys: user.apiKeys, verified: user.verified, id: user._id, - totalSize: req.user.totalSize + totalSize: user.totalSize }; } diff --git a/server/routes/aws.routes.js b/server/routes/aws.routes.js index f73e4fa4..19401d61 100644 --- a/server/routes/aws.routes.js +++ b/server/routes/aws.routes.js @@ -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.delete('/S3/:user_id?/:object_key', isAuthenticated, AWSController.deleteObjectFromS3); router.get('/S3/objects', AWSController.listObjectsInS3ForUserRequestHandler); export default router; From 09e403facfff65b0a52675cf203ae11a445ecbfc Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 25 Sep 2019 18:01:50 -0400 Subject: [PATCH 5/8] change file limit to 250 mb 1000 bytes was for testin --- client/modules/IDE/selectors/users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/modules/IDE/selectors/users.js b/client/modules/IDE/selectors/users.js index d597c894..7a2a9a1b 100644 --- a/client/modules/IDE/selectors/users.js +++ b/client/modules/IDE/selectors/users.js @@ -18,8 +18,8 @@ export const getCanUploadMedia = createSelector( export const getreachedTotalSizeLimit = createSelector( getTotalSize, (totalSize) => { - // if (totalSize > 250000000) return true; - if (totalSize > 1000) return true; + if (totalSize > 250000000) return true; + //if (totalSize > 1000) return true; return false; } ); From 59fe175ede253230959689337949cffe3bbdc29e Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Thu, 26 Sep 2019 15:06:43 -0400 Subject: [PATCH 6/8] Fix merge conflict for cherry picking 11833e9880257fe8164331689f77f8aa11df6a2a --- client/modules/IDE/actions/uploader.js | 6 ++--- client/modules/IDE/components/NewFileForm.jsx | 22 ++++++++++--------- .../modules/IDE/components/NewFolderForm.jsx | 22 ++++++++++--------- .../modules/IDE/components/NewFolderModal.jsx | 2 +- client/modules/IDE/components/Sidebar.jsx | 2 +- .../IDE/components/UploadFileModal.jsx | 4 ++-- client/modules/IDE/selectors/users.js | 8 ++++--- client/styles/components/_modal.scss | 6 ++--- server/controllers/aws.controller.js | 5 +++++ server/views/index.js | 1 + 10 files changed, 45 insertions(+), 33 deletions(-) diff --git a/client/modules/IDE/actions/uploader.js b/client/modules/IDE/actions/uploader.js index f2f12585..602ac3e5 100644 --- a/client/modules/IDE/actions/uploader.js +++ b/client/modules/IDE/actions/uploader.js @@ -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.'); }); } }; diff --git a/client/modules/IDE/components/NewFileForm.jsx b/client/modules/IDE/components/NewFileForm.jsx index d0a6b8ab..926a9925 100644 --- a/client/modules/IDE/components/NewFileForm.jsx +++ b/client/modules/IDE/components/NewFileForm.jsx @@ -22,16 +22,18 @@ class NewFileForm extends React.Component { handleSubmit(this.createFile)(data); }} > - - { this.fileName = element; }} - /> - +
    + + { this.fileName = element; }} + /> + +
    {name.touched && name.error && {name.error}} ); diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index 3490de64..92b91442 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -25,16 +25,18 @@ class NewFolderForm extends React.Component { } }} > - - { this.fileName = element; }} - {...domOnlyProps(name)} - /> - +
    + + { this.fileName = element; }} + {...domOnlyProps(name)} + /> + +
    {name.touched && name.error && {name.error}} ); diff --git a/client/modules/IDE/components/NewFolderModal.jsx b/client/modules/IDE/components/NewFolderModal.jsx index 60483ce8..4af43a89 100644 --- a/client/modules/IDE/components/NewFolderModal.jsx +++ b/client/modules/IDE/components/NewFolderModal.jsx @@ -16,7 +16,7 @@ class NewFolderModal extends React.Component {
    { this.newFolderModal = element; }} >
    -

    Add Folder

    +

    Create Folder

    diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index f6a5bdd1..61c61515 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -97,7 +97,7 @@ class Sidebar extends React.Component { onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} > - Add folder + Create folder
  • diff --git a/client/modules/IDE/components/UploadFileModal.jsx b/client/modules/IDE/components/UploadFileModal.jsx index 3e335fc0..9385d6aa 100644 --- a/client/modules/IDE/components/UploadFileModal.jsx +++ b/client/modules/IDE/components/UploadFileModal.jsx @@ -36,9 +36,9 @@ class UploadFileModal extends React.Component { { this.props.reachedTotalSizeLimit &&

    { - `You have reached the size limit for the number of files you can upload to your account. + `Error: You cannot upload any more files. You have reached the total size limit of 250MB. If you would like to upload more, please remove the ones you aren't using anymore by - looking through your ` + in your ` } assets {'.'} diff --git a/client/modules/IDE/selectors/users.js b/client/modules/IDE/selectors/users.js index 7a2a9a1b..c656e283 100644 --- a/client/modules/IDE/selectors/users.js +++ b/client/modules/IDE/selectors/users.js @@ -1,7 +1,9 @@ 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, @@ -10,7 +12,7 @@ export const getCanUploadMedia = createSelector( if (!authenticated) return false; // eventually do the same thing for verified when // email verification actually works - if (totalSize > 250000000) return false; + if (totalSize > limit) return false; return true; } ); @@ -18,8 +20,8 @@ export const getCanUploadMedia = createSelector( export const getreachedTotalSizeLimit = createSelector( getTotalSize, (totalSize) => { - if (totalSize > 250000000) return true; - //if (totalSize > 1000) return true; + if (totalSize > limit) return true; + // if (totalSize > 1000) return true; return false; } ); diff --git a/client/styles/components/_modal.scss b/client/styles/components/_modal.scss index b7b56f78..752b4434 100644 --- a/client/styles/components/_modal.scss +++ b/client/styles/components/_modal.scss @@ -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 { diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js index b4976a50..92564ec4 100644 --- a/server/controllers/aws.controller.js +++ b/server/controllers/aws.controller.js @@ -73,6 +73,11 @@ export function deleteObjectFromS3(req, res) { } 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'; diff --git a/server/views/index.js b/server/views/index.js index d8848351..156fcfdc 100644 --- a/server/views/index.js +++ b/server/views/index.js @@ -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 === 'false' ? false : true}; From 1107f7352ac903190130a64c743b18130d8d54d2 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 3 Mar 2020 16:33:52 -0500 Subject: [PATCH 7/8] Add changes for asset upload limit, after cherry-picking changes from asset-limit-with-lambda --- client/modules/IDE/actions/ide.js | 5 +++-- client/modules/IDE/components/AssetList.jsx | 17 ++++------------- client/modules/IDE/components/AssetSize.jsx | 12 +++++++----- client/modules/IDE/components/FileUploader.jsx | 2 +- client/modules/IDE/components/Sidebar.jsx | 2 +- .../modules/IDE/components/UploadFileModal.jsx | 12 ++++++++---- client/modules/IDE/reducers/assets.js | 5 +++-- client/modules/IDE/reducers/ide.js | 2 +- client/styles/components/_asset-size.scss | 9 +++------ server/views/index.js | 2 +- 10 files changed, 32 insertions(+), 36 deletions(-) diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index 8b453436..239dc6c7 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -75,9 +75,10 @@ export function closeNewFileModal() { }; } -export function openUploadFileModal() { +export function openUploadFileModal(parentId) { return { - type: ActionTypes.OPEN_UPLOAD_FILE_MODAL + type: ActionTypes.OPEN_UPLOAD_FILE_MODAL, + parentId }; } diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index 58d3909c..da834dc2 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -127,7 +127,8 @@ AssetListRowBase.propTypes = { url: PropTypes.string.isRequired, sketchId: PropTypes.string, sketchName: PropTypes.string, - name: PropTypes.string.isRequired + name: PropTypes.string.isRequired, + size: PropTypes.number.isRequired }).isRequired, deleteAssetRequest: PropTypes.func.isRequired, username: PropTypes.string.isRequired @@ -172,13 +173,9 @@ class AssetList extends React.Component { } render() { - const { assetList, totalSize } = this.props; + const { assetList } = this.props; return (

    - {/* Eventually, this copy should be Total / 250 MB Used */} - {this.hasAssets() && totalSize && -

    {`${prettyBytes(totalSize)} Total`}

    - } {this.getAssetsTitle()} @@ -188,7 +185,7 @@ class AssetList extends React.Component { - + @@ -214,20 +211,14 @@ AssetList.propTypes = { sketchName: PropTypes.string, sketchId: PropTypes.string })).isRequired, - totalSize: PropTypes.number, getAssets: PropTypes.func.isRequired, loading: PropTypes.bool.isRequired }; -AssetList.defaultProps = { - totalSize: undefined -}; - function mapStateToProps(state) { return { user: state.user, assetList: state.assets.list, - totalSize: state.user.totalSize, loading: state.loading }; } diff --git a/client/modules/IDE/components/AssetSize.jsx b/client/modules/IDE/components/AssetSize.jsx index db083708..2e4c1282 100644 --- a/client/modules/IDE/components/AssetSize.jsx +++ b/client/modules/IDE/components/AssetSize.jsx @@ -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 ( -
    +

    {currentSize} ({percent})

    Max: {sizeLimit}

    @@ -42,7 +44,7 @@ AssetSize.propTypes = { function mapStateToProps(state) { return { user: state.user, - totalSize: state.assets.totalSize, + totalSize: state.user.totalSize || state.assets.totalSize, }; } diff --git a/client/modules/IDE/components/FileUploader.jsx b/client/modules/IDE/components/FileUploader.jsx index 50fe6062..c9515f5c 100644 --- a/client/modules/IDE/components/FileUploader.jsx +++ b/client/modules/IDE/components/FileUploader.jsx @@ -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 diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index 61c61515..9a57ff4a 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -117,7 +117,7 @@ class Sidebar extends React.Component {
    NameName Size Sketch