diff --git a/.gitignore b/.gitignore index 1f438a47..68365c88 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ package-lock.json # IntelliJ .idea *.iml + +#MAC +.DS* diff --git a/js/rendererjs/plugins.js b/js/rendererjs/plugins.js index d6b416ca..2d9d5b50 100644 --- a/js/rendererjs/plugins.js +++ b/js/rendererjs/plugins.js @@ -150,6 +150,9 @@ export const pushToTop = (plugins, target) => export const getOrderedPlugins = (path, homePlugin) => { let plugins = scanFolder(path) + // Push the Terminal plugin to the bottom + plugins = pushToBottom(plugins, 'Inspector') + // Push the Terminal plugin to the bottom plugins = pushToBottom(plugins, 'Terminal') diff --git a/package.json b/package.json index d5b1c16c..57aad6ff 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,9 @@ "spectron": "^3.6.0", "uglify-js": "git://github.com/mishoo/UglifyJS2.git#harmony-v2.8.22", "uglifyjs-webpack-plugin": "^0.4.3", - "webpack": "^2.5.1" + "webpack": "^2.5.1", + "rc-tooltip": "^3.4.7", + "react-svg-buttons": "^0.4.0" }, "dependencies": { "babel-polyfill": "^6.9.1", diff --git a/plugins/Inspector/assets/button.png b/plugins/Inspector/assets/button.png new file mode 100644 index 00000000..56e6343a Binary files /dev/null and b/plugins/Inspector/assets/button.png differ diff --git a/plugins/Inspector/css/inspector.css b/plugins/Inspector/css/inspector.css new file mode 100644 index 00000000..8a10b823 --- /dev/null +++ b/plugins/Inspector/css/inspector.css @@ -0,0 +1,640 @@ +/* Style Guide: + * Transparent: 70% Opacity + * + * White: #FFFFFF + * Grey-White: #F5F5F5 + * Faint-Grey: #ECECEC + * Light-Grey: #DDDDDD + * Grey: #C5C5C5 + * Grey-Black: #4A4A4A + * Black: #000000 + * Neon-Green: #00CBA0 + * Venetian-Red: #CC0033 + */ + +.app { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.file-browser-container { + display: flex; + width: 100%; + height: 100%; +} +.redundancy-text { + font-size: 12px; + margin-right: 8px; + width: 35px; +} +.redundancy-status { + display: flex; + align-items: center; + justify-content: center; + padding-left: 15px; +} +.file-browser { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + overflow-x: hidden; + height: 100%; + width: 100%; +} +.filename .fa-folder { + cursor: pointer; +} +div[tabindex="1"]:focus { + outline: 0; +} +.selected { + border-left: 5px solid #00CBA0; + background-color: #F5F5F5 !important; +} +.dragtarget { + background-color: #F5F5F5 !important; +} +.file-controls { + z-index: 5; + position: absolute; + bottom: 0; + height: 60px; + width: 260px; + margin-left: auto; + margin-right: auto; + display: flex; + align-items: center; + justify-content: center; + background-color: #fff; +} +.transfer-status { + font-size: 12px; + margin-top: 10px; +} +.file-controls i { + color: #00CBA0; + cursor: pointer; + margin-left: 10px; + margin-right: 10px; +} + +@keyframes transfers-slidein { + from {width: 0} + to {width: 400px} +} + +.file-transfers { + width: 400px; + height: 100%; + box-shadow:inset 0px 0px 0px 1px #00CBA0; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + background-color: #C5C5C5; + + animation-name: transfers-slidein; + animation-duration: 0.3s; +} + +.transfer-list { + width: 100%; + margin: 0; + padding: 0; + list-style-type: none; + justify-content: center; + align-items: center; +} +.clear-downloads { + float: right; + margin-right: 15px; + margin-top: 5px; +} +.filetransfer { + margin: 0; + display: flex; + width: 100%; + padding-bottom: 10px; + border-bottom: 1px solid #C5C5C5; + align-items: center; + justify-content: space-around; + background-color: #ECECEC; + cursor: default; +} +.downloads .transfer-list li:hover { + background-color: #DDDDDD; +} +.downloads .transfer-list li { + cursor: pointer; +} +.transfername { + font-size: 17px; + margin-top: 10px; + margin-bottom: 10px; + overflow: hidden; + text-overflow: ellipsis; +} +.transfer-info { + width: 80%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; +} +.transfer-info .progress-container { + height: 12px; + width: 100%; + background-color: #C5C5C5; +} +.uploads, .downloads { + width: 100%; +} +.uploads h3, .downloads h3 { + margin-left: 25px; +} + +@keyframes modal-fadein { + from {opacity: 0} + to {opacity: 1} +} + +.modal { + position: absolute; + z-index: 1; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + + animation-name: modal-fadein; + animation-duration: 0.3s; +} + +.files-toolbar h3 { + font-size: 30px; + color: #fff; + margin: 0; + padding-bottom: 15px; +} + +.files-toolbar { + width: 100%; + height: 50px; + min-width: 800px; + white-space: nowrap; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #4a4a4a; + background: linear-gradient(to top, #5A5A5A 15%, #4A4A4A 85%); + color: #fff; + padding-bottom: 8px; + padding-top: 8px; + font-size: 16px; +} +.set-allowance-button { + display: inline-block; +} +.search-button { + display: inline-block; +} +.upload-button { + display: inline-block; +} +.upload-button:last-of-type { + position: absolute; + left: 0px; + top: 45px; + overflow: hidden; + padding: 5px 0px 0px; + max-height: 0px; + background-color: #575757; + transition: all 1s .1s; +} +.upload-button-container:hover .upload-button:last-of-type { + max-height: 100px; + padding: 23px 0px 5px; +} +.upload-button-container { + display: inline-block; + position: relative; + top: 0px; +} +.files-toolbar .buttons div { + text-align: center; + width: 100px; + cursor: pointer; +} +.fa-2x { + font-size: 1.5em; +} +.transfers-button { + display: inline-block; +} + +@keyframes notify-animation { + from { + width: 40px; + height: 40px; + } + to { + width: 20px; + height: 20px; + } +} + +.transfers-button > .badge { + background: #CC0033; + border-radius: 50%; + position: absolute; + top: 5px; + right: 25px; + width: 20px; + height: 20px; + vertical-align: center; + display: flex !important; + align-items: center; + justify-content: center; + font-size: 11px; + animation-name: notify-animation; + animation-duration: 0.5s; +} +.files-toolbar .buttons div[class$="button"]:hover { + opacity: 0.5; +} +.files-usage-info { + margin-left: 15px; +} +.files-toolbar .buttons div span { + display: block; + padding: 0; + margin-top: 2px; + margin-bottom: 2px; + font-size: 12px; + color: #f5f5f5; + user-select: none; +} +.files-toolbar .buttons div i { + color: #00CBA0; +} +.file-transfers .close-button { + position: absolute; + top: 12px; + right: 25px; + cursor: pointer; +} +.file-list h2 { + text-align: center; +} +.file-list { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + overflow-y: auto; + background-color: #C5C5C5; + margin: 0; + cursor: default; + -webkit-user-select: none; + font-size: 14px; +} +.file-list ul { + margin: 0; + list-style-type: none; + justify-content: flex-start; + width: 100%; + padding: 0; +} +.file-list .directory-infobar { + height: 35px; +} +.filesize { + font-size: 12px; +} +.file-list li { + display: flex; + height: 22px; + align-items: center; + justify-content: space-between; + background-color: #ECECEC; + padding-left: 10px; + padding-right: 10px; +} +.file-list .file-info { + display: flex; + align-items: center; + justify-content: center; +} +.file-buttons { + margin-bottom: 5px; + cursor: pointer; +} +.filename { + display: flex; + align-items: center; + justify-content: flex-start; +} +.filebrowser-file:hover { + background-color: #F5F5F5; +} +.file-list li i { + color: #00CBA0; + margin-left: 10px; + margin-right: 10px; +} +.search-field { + margin-top: 15px; + margin-bottom: 15px; + width: 80%; + display: flex; + align-items: center; + justify-content: center; +} +.search-field i { + display: inline-block; + margin-left: 5px; + margin-right: 5px; +} +.search-field input { + display: inline-block; + width: 80%; + height: 25px; + border: 2px solid #00CBA0; +} +.drag-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; +} + +.drag-overlay span { + color: #FFFFFF; + text-align: center; +} +.drag-overlay i { + color: #00CBA0; +} +.drag-overlay h3 { + margin-top: .2em; + margin-bottom: 0em; +} + +.upload-dialog { + padding-left: 25px; + padding-right: 25px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: #ECECEC; + width: 280px; + height: 190px; +} +.addfolder-dialog { + padding: 25px 25px 25px 25px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: #ECECEC; + width: 300px; + height: 200px; + border-radius: 4px; +} +.upload-dialog-buttons { + margin-top: 20px; +} +.upload-dialog-buttons > * { + margin-left: 5px; + margin-right: 5px; +} +.upload-dialog h1 { + margin: 0; + padding: 0; + margin-top: 10px; + margin-bottom: 10px; +} +.storage-plans { + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; + margin-top: 10px; + margin-bottom: 10px; + padding-bottom: 10px; + padding-top: 10px; +} +.plan { + margin: 5px 5px 5px 5px; + width: 100px; + height: 80px; + text-align: center; +} +.allowance-dialog h3 { + padding: 0; + margin: 0; + margin-bottom: 15px; +} +.allowance-dialog.unlock-warning { + justify-content: center; +} +.unlock-warning-head { + margin-top: 10px; +} +.allowance-message > .footnote { + font-size: 14px; +} +.allowance-message { + margin-left: 20px; + margin-right: 20px; + margin-bottom: 25px; +} +.allowance-dialog p { + margin-top: 10px; + text-align: left; +} +.allowance-warning { + color: #A71A18; +} +.allowance-buttons button { + width: 100px; + height: 25px; + margin-left: 5px; + margin-right: 5px; +} +.allowance-dialog form { + width: 400px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.allowance-dialog form input { + display: inline-block; + text-align: center; + margin-top: 10px; + margin-bottom: 10px; + margin-right: 4px; + width: 187px; +} +.estimates { + margin-top: 35px; + width: 230px; +} +.allowance-dialog form { + margin-top: 10px; + margin-bottom: 10px; +} +.allowance-input { + padding-right: 80px; +} + +@keyframes allowance-dialog-popin { + from { transform: scale(0.2) } + to { transform: scale(1) } +} + +.allowance-dialog { + padding: 40px 40px 40px 40px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-around; + background-color: #ECECEC; + width: 50%; + min-width: 700px; + height: 400px; + z-index: 10; + + animation-name: allowance-dialog-popin; + animation-duration: 0.2s; + + border-radius: 4px; +} + +.allowance-input span { + display: inline-block; +} +.estimate-label { + text-align: left; +} +.estimate-content { + text-align: right; +} +.estimates td { + padding-left: 5px; + padding-right: 5px; +} +.delete-dialog { + margin-left: 15px; + margin-right: 15px; + padding-left: 15px; + padding-right: 15px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: #ECECEC; + width: 50%; + height: 50%; +} +.delete-buttons { + margin-top: 35px; +} +.delete-buttons button { + margin-left: 10px; + margin-right: 10px; +} +.rename-dialog { + margin-left: 15px; + margin-right: 15px; + padding-left: 15px; + padding-right: 15px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: #ECECEC; + width: 50%; + height: 50%; +} +.rename-buttons { + margin: auto; + margin-top: 35px; + text-align: center; +} +.rename-buttons button { + margin-left: 10px; + margin-right: 10px; +} +.file-buttons { + display: flex; +} +.file-buttons i { + color: #00CBA0; +} +.unlock-dialog { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: #C5C5C5; +} +.file-buttons div { + margin: 10px 5px 5px 10px; +} +.rename-field { + margin: auto; + margin-top: 15px; + margin-bottom: 15px; + width: 80%; + display: flex; + align-items: center; + justify-content: center; +} +.rename-field input { + display: inline-block; + width: 80%; + height: 25px; + border: 2px solid #00CBA0; +} +.rename-form { + display: inline-block; + width: 80%; +} +.buttons { + width: 500px; + display: flex; +} + +#react-paginate ul { + display: inline-block; + padding-left: 15px; + padding-right: 15px; +} + +#react-paginate li { + display: inline-block; +} + +#react-paginate .break a { + cursor: default; +} diff --git a/plugins/Inspector/index.html b/plugins/Inspector/index.html new file mode 100644 index 00000000..95cd46bc --- /dev/null +++ b/plugins/Inspector/index.html @@ -0,0 +1,19 @@ + + + + Files + + + + + + + + + +
+ + + + + diff --git a/plugins/Inspector/js/actions/files.js b/plugins/Inspector/js/actions/files.js new file mode 100644 index 00000000..a4597299 --- /dev/null +++ b/plugins/Inspector/js/actions/files.js @@ -0,0 +1,37 @@ +import * as constants from '../constants/files.js' + +export const getFiles = () => ({ + type: constants.GET_FILES, +}) +export const receiveFiles = (files) => ({ + type: constants.RECEIVE_FILES, + files, +}) +export const receiveFileDetail = (detail) => ({ + type: constants.RECEIVE_FILE_DETAIL, + ...detail, +}) +export const fetchData = () => ({ + type: constants.FETCH_DATA, +}) +export const showFileDetail = (siapath) => ({ + type: constants.SHOW_FILE_DETAIL, + siapath, +}) +export const closeFileDetail = () => ({ + type: constants.CLOSE_FILE_DETAIL, +}) +export const fetchFileDetail = (siapath, pagingNum, current) => ({ + type: constants.GET_FILE_DETAIL, + siapath, + pagingNum, + current, +}) +export const setDragFolderTarget = (target) => ({ + type: constants.SET_DRAG_FOLDER_TARGET, + target, +}) +export const setPath = (path) => ({ + type: constants.SET_PATH, + path, +}) diff --git a/plugins/Inspector/js/components/addfolderbutton.js b/plugins/Inspector/js/components/addfolderbutton.js new file mode 100644 index 00000000..8fd0e0bd --- /dev/null +++ b/plugins/Inspector/js/components/addfolderbutton.js @@ -0,0 +1,13 @@ +import React from 'react' + +const AddFolderButton = ({actions}) => { + const handleClick = () => actions.showAddFolderDialog() + return ( +
+ + New Folder +
+ ) +} + +export default AddFolderButton diff --git a/plugins/Inspector/js/components/addfolderdialog.js b/plugins/Inspector/js/components/addfolderdialog.js new file mode 100644 index 00000000..c931e527 --- /dev/null +++ b/plugins/Inspector/js/components/addfolderdialog.js @@ -0,0 +1,30 @@ +import React from 'react' + +const AddFolderDialog = ({actions}) => { + const onConfirmClick = (e) => { + e.preventDefault() + actions.addFolder(e.target.name.value) + actions.hideAddFolderDialog() + } + const onCancelClick = () => actions.hideAddFolderDialog() + return ( +
+
+
+ Enter a name for the new folder: +
+
+
+ +
+
+ + +
+
+
+
+ ) +} + +export default AddFolderDialog diff --git a/plugins/Inspector/js/components/allowanceconfirmation.js b/plugins/Inspector/js/components/allowanceconfirmation.js new file mode 100644 index 00000000..f68e0e5d --- /dev/null +++ b/plugins/Inspector/js/components/allowanceconfirmation.js @@ -0,0 +1,42 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const ConfirmationDialog = ({allowance, onConfirmClick, onCancelClick}) => { + const confirmationStyle = { + 'padding': '40px 40px 40px 40px', + 'display': 'flex', + 'flexDirection': 'column', + 'alignItems': 'center', + 'justifyContent': 'space-around', + 'backgroundColor': '#ececec', + 'width': '50%', + 'height': '400px', + } + const buttonStyle = { + 'marginLeft': '5px', + 'marginRight': '5px', + } + const confirmationTextStyle = { + 'marginBottom': '20px', + 'fontSize': '24px', + } + return ( +
+

+ Please confirm that you would like to set aside {allowance} SC for storage on the Sia network. +

+
+ + +
+
+ ) +} + +ConfirmationDialog.propTypes = { + allowance: PropTypes.string.isRequired, + onConfirmClick: PropTypes.func.isRequired, + onCancelClick: PropTypes.func.isRequired, +} + +export default ConfirmationDialog diff --git a/plugins/Inspector/js/components/allowancedialog.js b/plugins/Inspector/js/components/allowancedialog.js new file mode 100644 index 00000000..baf22ed6 --- /dev/null +++ b/plugins/Inspector/js/components/allowancedialog.js @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types' +import React from 'react' +import UnlockWarning from './unlockwarning.js' +import ConfirmationDialog from './allowanceconfirmation.js' +import BigNumber from 'bignumber.js' + +const AllowanceDialog = ({confirming, confirmationAllowance, unlocked, synced, feeEstimate, storageEstimate, actions}) => { + const onCancelClick = () => actions.closeAllowanceDialog() + const onConfirmationCancel = () => actions.hideAllowanceConfirmation() + const onConfirmClick = () => actions.setAllowance(confirmationAllowance) + const onAcceptClick = (e) => { + e.preventDefault() + actions.showAllowanceConfirmation(e.target.allowance.value) + } + const onAllowanceChange = (e) => actions.getStorageEstimate(e.target.value) + const dialogContents = confirming ? ( + + ) : ( +
+

Buy storage on the Sia Decentralized Network

+
+

You need to allocate funds to upload and download on Sia. Your allowance remains locked for 3 months. Unspent funds are then refunded*. You can increase your allowance at any time.

+

Your storage allowance automatically refills every 6 weeks. Your computer must be online with your wallet unlocked to complete the refill. If Sia fails to refill the allowance by the end of the lock-in period, your data may be lost.

+

*contract fees are non-refundable. They will be subtracted from the allowance that you set.

+
+
+
+ + SC +
+
+ + +
+ + + + + + + + + +
Estimated Fees{new BigNumber(feeEstimate).round(2).toString()} SC
Estimated Storage{storageEstimate}
+
+
+ ) + + return ( +
+ {unlocked && synced ? dialogContents : } +
+ ) +} + +AllowanceDialog.propTypes = { + confirmationAllowance: PropTypes.string.isRequired, + confirming: PropTypes.bool.isRequired, + unlocked: PropTypes.bool.isRequired, + synced: PropTypes.bool.isRequired, + feeEstimate: PropTypes.number.isRequired, + storageEstimate: PropTypes.string.isRequired, +} + +export default AllowanceDialog diff --git a/plugins/Inspector/js/components/app.js b/plugins/Inspector/js/components/app.js new file mode 100644 index 00000000..7b727581 --- /dev/null +++ b/plugins/Inspector/js/components/app.js @@ -0,0 +1,17 @@ +import PropTypes from 'prop-types' +import React from 'react' +import FileBrowser from '../containers/filebrowser.js' +import AllowanceDialog from '../containers/allowancedialog.js' + +const FilesApp = ({showAllowanceDialog}) => ( +
+ {showAllowanceDialog ? : null} + +
+) + +FilesApp.propTypes = { + showAllowanceDialog: PropTypes.bool.isRequired, +} + +export default FilesApp diff --git a/plugins/Inspector/js/components/contractorstatus.js b/plugins/Inspector/js/components/contractorstatus.js new file mode 100644 index 00000000..8748e07d --- /dev/null +++ b/plugins/Inspector/js/components/contractorstatus.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const ContractorStatus = ({settingAllowance, contractCount}) => ( +
+ { settingAllowance ? ( +
+ + Forming Contracts... +
+ ) : ( {contractCount} contracts ) + } +
+) + +ContractorStatus.propTypes = { + settingAllowance: PropTypes.bool.isRequired, + contractCount: PropTypes.number.isRequired, +} + +export default ContractorStatus + diff --git a/plugins/Inspector/js/components/deletedialog.js b/plugins/Inspector/js/components/deletedialog.js new file mode 100644 index 00000000..e2c159b8 --- /dev/null +++ b/plugins/Inspector/js/components/deletedialog.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { List } from 'immutable' + +const DeleteDialog = ({files, actions}) => { + const onYesClick = () => { + files.map(actions.deleteFile) + actions.hideDeleteDialog() + } + const onNoClick = () => actions.hideDeleteDialog() + return ( +
+
+

Confirm Deletion

+
+ Are you sure you want to delete {files.size} {files.size === 1 ? ' file' : ' files'} +
+
+ + +
+
+
+ ) +} + +DeleteDialog.propTypes = { + files: PropTypes.instanceOf(List).isRequired, +} + +export default DeleteDialog diff --git a/plugins/Inspector/js/components/directoryinfobar.js b/plugins/Inspector/js/components/directoryinfobar.js new file mode 100644 index 00000000..128c6b0e --- /dev/null +++ b/plugins/Inspector/js/components/directoryinfobar.js @@ -0,0 +1,42 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const colorBackDisabled = '#C5C5C5' +const colorBackEnabled = '#00CBA0' + +const DirectoryInfoBar = ({path, nfiles, onBackClick, setDragFolderTarget}) => { + const backButtonStyle = { + color: (() => { + if (path === '') { + return colorBackDisabled + } + return colorBackEnabled + })(), + } + // handle file drag onto the info bar: move the file into the parent + // directory + const handleDragOver = () => { + setDragFolderTarget('../') + } + return ( +
  • +
    + + Back +
    +
    + {path} + {nfiles} {nfiles === 1 ? 'file' : 'files' } +
    +
  • + ) +} + +DirectoryInfoBar.propTypes = { + path: PropTypes.string.isRequired, + nfiles: PropTypes.number.isRequired, + onBackClick: PropTypes.func.isRequired, + setDragFolderTarget: PropTypes.func.isRequired, +} + +export default DirectoryInfoBar diff --git a/plugins/Inspector/js/components/downloadlist.js b/plugins/Inspector/js/components/downloadlist.js new file mode 100644 index 00000000..07027679 --- /dev/null +++ b/plugins/Inspector/js/components/downloadlist.js @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types' +import React from 'react' +import TransferList from './transferlist.js' +import { List } from 'immutable' + +const DownloadList = ({downloads, onDownloadClick, onClearClick}) => ( +
    +

    Downloads

    + + +
    +) + +DownloadList.propTypes = { + downloads: PropTypes.instanceOf(List).isRequired, + onDownloadClick: PropTypes.func, + onClearClick: PropTypes.func, +} + +export default DownloadList diff --git a/plugins/Inspector/js/components/dragoverlay.js b/plugins/Inspector/js/components/dragoverlay.js new file mode 100644 index 00000000..ac673569 --- /dev/null +++ b/plugins/Inspector/js/components/dragoverlay.js @@ -0,0 +1,12 @@ +import React from 'react' + +const DragOverlay = () => ( +
    + + +

    Drag to Upload

    +
    +
    +) + +export default DragOverlay diff --git a/plugins/Inspector/js/components/file.js b/plugins/Inspector/js/components/file.js new file mode 100644 index 00000000..64923514 --- /dev/null +++ b/plugins/Inspector/js/components/file.js @@ -0,0 +1,67 @@ +import PropTypes from 'prop-types' +import React from 'react' +import RedundancyStatus from './redundancystatus.js' + +const File = ({filename, type, selected, isDragTarget, filesize, available, redundancy, uploadprogress, onDoubleClick, onClick, setDragUploadEnabled, setDragFolderTarget, setDragFileOrigin, handleDragRename, isSiaUIFolder }) => { + const handleDrag = () => { + } + const handleDragStart = () => { + setDragUploadEnabled(false) + setDragFileOrigin({type: type, name: filename, isSiaUIFolder: isSiaUIFolder}) + setDragFolderTarget('') + } + const handleDragEnd = () => { + setDragUploadEnabled(true) + handleDragRename() + } + const handleDragOver = () => { + if (type === 'directory') { + setDragFolderTarget(filename) + } else { + setDragFolderTarget('') + } + } + const fileClass = (() => { + if (isDragTarget) { + return 'filebrowser-file dragtarget' + } + if (selected) { + return 'filebrowser-file selected' + } + return 'filebrowser-file' + })() + return ( +
  • +
    + {type === 'file' ? : } +
    {filename}
    +
    +
    + {filesize} + +
    +
  • + ) +} + +File.propTypes = { + filename: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + filesize: PropTypes.string.isRequired, + available: PropTypes.bool.isRequired, + redundancy: PropTypes.number, + uploadprogress: PropTypes.number, + selected: PropTypes.bool.isRequired, + onDoubleClick: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, +} + +export default File diff --git a/plugins/Inspector/js/components/filebrowser.js b/plugins/Inspector/js/components/filebrowser.js new file mode 100644 index 00000000..ff10d149 --- /dev/null +++ b/plugins/Inspector/js/components/filebrowser.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types' +import React from 'react' +import FileList from '../containers/filelist.js' +import FileTransfers from '../containers/filetransfers.js' +import DragOverlay from './dragoverlay.js' + +const FileBrowser = ({dragging, dragUploadEnabled, showFileTransfers, actions}) => { + const onDragOver = (e) => { + if (!dragUploadEnabled) { + return + } + e.preventDefault() + actions.setDragging() + } + const onDrop = (e) => { + if (!dragUploadEnabled) { + return + } + e.preventDefault() + // Convert file list into a list of file paths. + actions.showUploadDialog(Array.from(e.dataTransfer.files, (file) => file.path)) + } + const onDragLeave = (e) => { + if (!dragUploadEnabled) { + return + } + e.preventDefault() + } + const onKeyDown = (e) => { + // Deselect all files when ESC is pressed. + if (e.keyCode === 27) { + actions.deselectAll() + } + } + return ( +
    +
    + {dragging ? : null} + +
    + {showFileTransfers ? : null} +
    + ) +} + +FileBrowser.propTypes = { + dragging: PropTypes.bool.isRequired, + settingAllowance: PropTypes.bool.isRequired, + showRenameDialog: PropTypes.bool.isRequired, + showUploadDialog: PropTypes.bool.isRequired, + showDeleteDialog: PropTypes.bool.isRequired, + showFileTransfers: PropTypes.bool.isRequired, + dragUploadEnabled: PropTypes.bool.isRequired, +} + +export default FileBrowser diff --git a/plugins/Inspector/js/components/filecontrols.js b/plugins/Inspector/js/components/filecontrols.js new file mode 100644 index 00000000..78c078c2 --- /dev/null +++ b/plugins/Inspector/js/components/filecontrols.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Set } from 'immutable' +import Path from 'path' + +const FileControls = ({files, actions}) => { + const onDownloadClick = () => { + const downloadpath = SiaAPI.openFile({ + title: 'Where should we download?', + properties: ['openDirectory', 'createDirectories'], + }) + if (downloadpath.length === 0) { + // No files selected, nop + return + } + files.forEach(async (file) => { + actions.downloadFile(file, Path.join(downloadpath[0], Path.basename(file.siapath))) + await new Promise((resolve) => setTimeout(resolve, 300)) + }) + } + const onDeleteClick = () => { + actions.showDeleteDialog(files.toList()) + } + const onRenameClick = () => { + actions.showRenameDialog(files.first()) + } + return ( +
    + {files.size} {files.size === 1 ? ' item' : ' items' } selected +
    + +
    + {files.size === 1 ? ( +
    + +
    + ) : null} +
    + +
    +
    + ) +} + +FileControls.propTypes = { + files: PropTypes.instanceOf(Set).isRequired, +} + +export default FileControls + diff --git a/plugins/Inspector/js/components/filedetail.js b/plugins/Inspector/js/components/filedetail.js new file mode 100644 index 00000000..071640ff --- /dev/null +++ b/plugins/Inspector/js/components/filedetail.js @@ -0,0 +1,138 @@ +import PropTypes from 'prop-types' +import React from 'react' +import FileDetailTip from './filedetailtip.js' +import * as constants from '../constants/files.js' +import Tooltip from 'rc-tooltip' +import { CloseButton } from 'react-svg-buttons' + +const logViewStyle = { + position: 'absolute', + top: '20px', + bottom: '0', + left: '20px', + right: '20px', + margin: '0', + padding: '0', + overflowY: 'scroll', + whiteSpace: 'pre', + fontSize: '12px', + fontFamily: 'monospace', +} + +const reapirFileViewStyle = { + margin: '10px 20px', +} + +const repaireChunkStyle = { + width: '10px', + height: '10px', + float: 'left', + border: 'solid 1px #001f3f', +} + +const repairChunkRepairedStyle = { + ...repaireChunkStyle, + background: '#01FF70', +} + +const repairChunkRepairingStyle = { + ...repaireChunkStyle, + background: '#FF851B', +} + +const repairChunkQueuedStyle = { + ...repaireChunkStyle, + background: 'white', +} + +const clearBothStyle = { + clear: 'both', +} + +const closeButtonStyle = { + float: 'right', + cursor: 'pointer', +} + +const FileDetail = ({showDetailPath, showDetailFile, current, actions}) => { + const closeDetail = actions.closeFileDetail + + if (!showDetailFile) { + actions.fetchFileDetail(showDetailPath, constants.DEFAULT_PAGE_SIZE, 1) + return ( +
    +
    + +
    +
    +
      +

      Loading file detail...

      +
    +
    + ) + } + + const getHost = (idx) => showDetailFile.details.hosts[idx] + + const getChunkStyle = (chunk) => { + const onlinePiece = chunk.reduce((sum, piece) => piece.some((ele) => getHost(ele).host && !getHost(ele).isoffline) ? sum+1: sum, 0) + if (onlinePiece === chunk.length) { + return repairChunkRepairedStyle + } + if (onlinePiece === 0) { + return repairChunkQueuedStyle + } + return repairChunkRepairingStyle + } + const links = Array.from(new Array(showDetailFile.totalpages), (val, index) => { + if (index+1 === current) { + return ( {index+1} ) + } + const onChange = () => { + actions.fetchFileDetail(showDetailPath, constants.DEFAULT_PAGE_SIZE, index+1) + } + return ( {index+1} ) + }) + + return ( +
    +
    + +
    +
    +

    + file: {showDetailPath} +

    + +
    + Pages: {links} +
    + +
    + {showDetailFile.details.chunks.map((chunk, i) => ( + + } + > + + ) + )} +
    +
    +
    + ) +} + +FileDetail.propTypes = { + showDetailPath: PropTypes.string.isRequired, +} + +export default FileDetail diff --git a/plugins/Inspector/js/components/filedetailtip.js b/plugins/Inspector/js/components/filedetailtip.js new file mode 100644 index 00000000..d0154295 --- /dev/null +++ b/plugins/Inspector/js/components/filedetailtip.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types' +import React from 'react' +import * as constants from '../constants/files.js' + +const pieceOnlineStyle = { + color: '#01FF70', +} + +const pieceOfflineStyle = { + color: '#dc143c', +} + +const pieceEmptyStyle = { + color: 'white', +} + +const FileDetailTip = ({chunk, index, current, getHost}) => { + const tip = chunk.map((piece, i) => { + if (!piece || piece.length === 0) { + return (
    Empty
    ) + } + const line = piece.map((ele, j) => { + let s + if (getHost(ele).isoffline) { + s = pieceOfflineStyle + } else { + s = pieceOnlineStyle + } + return ({getHost(ele).host};) + }) + return (
    {i}: {line}
    ) + }) + + return ( +
    +
    ChunkId : {(current - 1) * constants.DEFAULT_PAGE_SIZE + index}
    + {tip} +
    + ) +} + +FileDetailTip.propTypes = { + chunk: PropTypes.array.isRequired, + index: PropTypes.number.isRequired, + current: PropTypes.number.isRequired, + getHost: PropTypes.func.isRequired, +} + +export default FileDetailTip diff --git a/plugins/Inspector/js/components/filelist.js b/plugins/Inspector/js/components/filelist.js new file mode 100644 index 00000000..1fb35116 --- /dev/null +++ b/plugins/Inspector/js/components/filelist.js @@ -0,0 +1,141 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { List, Set } from 'immutable' +import File from './file.js' +import Path from 'path' +import FileControls from '../containers/filecontrols.js' +import DirectoryInfoBar from './directoryinfobar.js' +import FileDetail from '../containers/filedetail.js' + +const FileList = ({files, selected, searchResults, path, showSearchField, dragFileOrigin, dragFolderTarget, showDetailPath, actions}) => { + const onBackClick = () => { + // remove a trailing slash if it exists + const cleanPath = path.replace(/\/$/, '') + + if (cleanPath === '') { + return + } + + // find the parent directory and set the new path + const pathComponents = cleanPath.split('/') + if (pathComponents.length < 2) { + actions.setPath('') + } else { + pathComponents.pop() + actions.setPath(pathComponents.join('/')) + } + } + + if (files === null) { + return ( +
    +
      +

      Loading files...

      +
    +
    + ) + } + + if (showDetailPath) { + return ( +
    + +
    + ) + } + + let filelistFiles + if (showSearchField) { + filelistFiles = searchResults + } else { + filelistFiles = files + } + const fileElements = filelistFiles.map((file, key) => { + const isSelected = selected.map((selectedfile) => selectedfile.name).includes(file.name) + const onFileClick = () => { + // a show file detail action + if (file.type === 'directory') { + return + } + actions.showFileDetail(file.siapath) + } + const onDoubleClick = (e) => { + e.stopPropagation() + if (file.type === 'directory') { + actions.setPath(file.siapath) + } + } + const handleDragRename = () => { + if (typeof dragFileOrigin === 'undefined' || dragFolderTarget === '') { + return + } + if (dragFileOrigin.name === dragFolderTarget) { + return + } + if (dragFolderTarget === '../' && path === '') { + return + } + if (selected.size > 0) { + selected.forEach((selectedfile) => { + const destSiapath = Path.posix.join(path, dragFolderTarget, selectedfile.name) + actions.renameFile(selectedfile, destSiapath) + if (selected.type === 'directory' && !selected.isSiaUIFolder) { + actions.deleteSiaUIFolder(sourceSiapath) + } + }) + } else { + const sourceSiapath = Path.posix.join(path, dragFileOrigin.name) + const destSiapath = Path.posix.join(path, dragFolderTarget, dragFileOrigin.name) + actions.renameFile({type: dragFileOrigin.type, siapath: sourceSiapath, isSiaUIFolder: dragFileOrigin.isSiaUIFolder}, destSiapath) + if (dragFileOrigin.type === 'directory' && !dragFileOrigin.isSiaUIFolder) { + actions.deleteSiaUIFolder(sourceSiapath) + } + } + actions.getFiles() + actions.setDragFolderTarget('') + actions.setDragFileOrigin({}) + } + return ( + + ) + }) + + return ( +
    +
      + + { fileElements.size > 0 ? fileElements :

      No files uploaded

      } +
    + {selected.size > 0 ? : null} +
    + ) +} + +FileList.propTypes = { + files: PropTypes.instanceOf(List), + selected: PropTypes.instanceOf(Set).isRequired, + searchResults: PropTypes.instanceOf(List), + path: PropTypes.string.isRequired, + showSearchField: PropTypes.bool.isRequired, + dragFileOrigin: PropTypes.object.isRequired, + dragFolderTarget: PropTypes.string.isRequired, +} + +export default FileList diff --git a/plugins/Inspector/js/components/filetransfers.js b/plugins/Inspector/js/components/filetransfers.js new file mode 100644 index 00000000..6a5c5d29 --- /dev/null +++ b/plugins/Inspector/js/components/filetransfers.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { List } from 'immutable' +import UploadList from './uploadlist.js' +import DownloadList from './downloadlist.js' +import { shell } from 'electron' + +const FileTransfers = ({uploads, downloads, actions}) => { + const onCloseClick = () => actions.hideFileTransfers() + const onDownloadClick = (download) => () => shell.showItemInFolder(download.destination) + const onDownloadsClearClick = () => { + actions.clearDownloads() + actions.getDownloads() + } + return ( +
    +
    + +
    + {downloads.size === 0 && uploads.size === 0 ? ( +

    No file transfers in progress.

    + ) : null + } + {downloads.size > 0 ? : null} + {uploads.size > 0 ? : null} +
    + ) +} + +FileTransfers.propTypes = { + uploads: PropTypes.instanceOf(List).isRequired, + downloads: PropTypes.instanceOf(List).isRequired, +} + +export default FileTransfers diff --git a/plugins/Inspector/js/components/progressbar.js b/plugins/Inspector/js/components/progressbar.js new file mode 100644 index 00000000..85d41ed9 --- /dev/null +++ b/plugins/Inspector/js/components/progressbar.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const ProgressBar = ({progress}) => { + const style = { + width: progress.toString() + '%', + height: '100%', + transition: 'width 200ms', + backgroundColor: '#00CBA0', + } + return ( +
    +
    +
    + ) +} + +ProgressBar.propTypes = { + progress: PropTypes.number.isRequired, +} + +export default ProgressBar diff --git a/plugins/Inspector/js/components/redundancystatus.js b/plugins/Inspector/js/components/redundancystatus.js new file mode 100644 index 00000000..b88fb314 --- /dev/null +++ b/plugins/Inspector/js/components/redundancystatus.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const colorNotAvailable = '#FF8080' +const colorGoodRedundancy = '#00CBA0' +const colorNegativeRedundancy = '#b7afaf' + +const RedundancyStatus = ({available, redundancy, uploadprogress}) => { + const indicatorStyle = { + opacity: (() => { + if (!available || redundancy < 1.0) { + return 1 + } + if (uploadprogress > 100) { + return 1 + } + return uploadprogress/100 + })(), + color: (() => { + if (redundancy < 0) { + return colorNegativeRedundancy + } + if (!available || redundancy < 1.0) { + return colorNotAvailable + } + return colorGoodRedundancy + })(), + } + return ( +
    + + {redundancy > 0 ? redundancy + 'x' : '--'} +
    + ) +} + +RedundancyStatus.propTypes = { + available: PropTypes.bool.isRequired, + redundancy: PropTypes.number.isRequired, + uploadprogress: PropTypes.number.isRequired, +} + +export default RedundancyStatus + diff --git a/plugins/Inspector/js/components/renamedialog.js b/plugins/Inspector/js/components/renamedialog.js new file mode 100644 index 00000000..9f3a7fd9 --- /dev/null +++ b/plugins/Inspector/js/components/renamedialog.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Path from 'path' + +const RenameDialog = ({file, actions}) => { + const onYesClick = (e) => { + e.preventDefault() + actions.renameFile(file, Path.posix.join(Path.posix.dirname(file.siapath), e.target.newname.value)) + } + const onNoClick = () => actions.hideRenameDialog() + return ( +
    +
    +
    + Enter a new name for {Path.basename(file.siapath)}: +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    + ) +} + +RenameDialog.propTypes = { + file: PropTypes.object.isRequired, +} + +export default RenameDialog diff --git a/plugins/Inspector/js/components/searchbutton.js b/plugins/Inspector/js/components/searchbutton.js new file mode 100644 index 00000000..fa41b9f2 --- /dev/null +++ b/plugins/Inspector/js/components/searchbutton.js @@ -0,0 +1,16 @@ +import React from 'react' + +const SearchButton = ({path, actions}) => { + const handleClick = () => { + actions.toggleSearchField() + actions.setSearchText('', path) + } + return ( +
    + + Search Files +
    + ) +} + +export default SearchButton diff --git a/plugins/Inspector/js/components/searchfield.js b/plugins/Inspector/js/components/searchfield.js new file mode 100644 index 00000000..1f2d5638 --- /dev/null +++ b/plugins/Inspector/js/components/searchfield.js @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const SearchField = ({searchText, path, actions}) => { + const onSearchChange = (e) => actions.setSearchText(e.target.value, path) + return ( +
    + + +
    + ) +} + +SearchField.propTypes = { + searchText: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, +} +export default SearchField diff --git a/plugins/Inspector/js/components/setallowancebutton.js b/plugins/Inspector/js/components/setallowancebutton.js new file mode 100644 index 00000000..e30750f2 --- /dev/null +++ b/plugins/Inspector/js/components/setallowancebutton.js @@ -0,0 +1,13 @@ +import React from 'react' + +const SetAllowanceButton = ({actions}) => { + const handleClick = () => actions.showAllowanceDialog() + return ( +
    + + Create Allowance +
    + ) +} + +export default SetAllowanceButton diff --git a/plugins/Inspector/js/components/transfer.js b/plugins/Inspector/js/components/transfer.js new file mode 100644 index 00000000..b65d88e6 --- /dev/null +++ b/plugins/Inspector/js/components/transfer.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types' +import React from 'react' +import ProgressBar from './progressbar.js' + +const Transfer = ({name, progress, status, onClick}) => ( +
  • +
    +
    {name}
    + + {status} +
    +
  • +) + +Transfer.propTypes = { + name: PropTypes.string.isRequired, + progress: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, +} + +export default Transfer diff --git a/plugins/Inspector/js/components/transferlist.js b/plugins/Inspector/js/components/transferlist.js new file mode 100644 index 00000000..67529fe3 --- /dev/null +++ b/plugins/Inspector/js/components/transferlist.js @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { List } from 'immutable' +import Transfer from './transfer.js' + +const defaultTransferClick = () => () => {} + +const TransferList = ({transfers, onTransferClick = defaultTransferClick}) => { + const transferComponents = transfers.map((transfer, key) => ( + + )) + return ( +
      + {transferComponents} +
    + ) +} + +TransferList.propTypes = { + transfers: PropTypes.instanceOf(List).isRequired, + onTransferClick: PropTypes.func, +} + +export default TransferList diff --git a/plugins/Inspector/js/components/transfersbutton.js b/plugins/Inspector/js/components/transfersbutton.js new file mode 100644 index 00000000..559d0fb1 --- /dev/null +++ b/plugins/Inspector/js/components/transfersbutton.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const FileTransfersButton = ({unread, actions}) => { + const onTransfersClick = () => actions.toggleFileTransfers() + return ( +
    + + {unread > 0 ? ( + {unread > 10 ? '10+' : unread} + ) : null} + File Transfers +
    + ) +} + +FileTransfersButton.propTypes = { + unread: PropTypes.number.isRequired, +} + +export default FileTransfersButton diff --git a/plugins/Inspector/js/components/unlockwarning.js b/plugins/Inspector/js/components/unlockwarning.js new file mode 100644 index 00000000..618d76ea --- /dev/null +++ b/plugins/Inspector/js/components/unlockwarning.js @@ -0,0 +1,17 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const UnlockWarning = ({onClick}) => ( +
    +

    Your wallet must be unlocked and synchronized to buy storage.

    +
    + +
    +
    +) + +UnlockWarning.propTypes = { + onClick: PropTypes.func.isRequired, +} + +export default UnlockWarning diff --git a/plugins/Inspector/js/components/uploadbutton.js b/plugins/Inspector/js/components/uploadbutton.js new file mode 100644 index 00000000..c9cc3b5c --- /dev/null +++ b/plugins/Inspector/js/components/uploadbutton.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const minimumContracts = 14 + +const UploadButton = ({contracts = minimumContracts, actions}) => { + const onUploadClick = (type) => () => { + if (contracts < minimumContracts) { + SiaAPI.showError({ + title: 'Sia-UI files error', + content: 'Not enough contracts to upload. You must buy storage before uploading, or wait for contracts to form.', + }) + return + } + let dialogProperties + if (type === 'folder') { + dialogProperties = ['openDirectory'] + } else if (type === 'file') { + dialogProperties = ['openFile', 'multiSelections'] + } + const filepaths = SiaAPI.openFile({ + title: 'Choose a ' + type + ' to upload', + properties: dialogProperties, + }) + if (filepaths) { + actions.showUploadDialog(filepaths) + } + } + return ( +
    +
    + + Upload Files +
    +
    + + Upload Folder +
    +
    + ) +} + +UploadButton.propTypes = { + contracts: PropTypes.number, +} + +export default UploadButton diff --git a/plugins/Inspector/js/components/uploaddialog.js b/plugins/Inspector/js/components/uploaddialog.js new file mode 100644 index 00000000..a4e3f0c5 --- /dev/null +++ b/plugins/Inspector/js/components/uploaddialog.js @@ -0,0 +1,36 @@ +import PropTypes from 'prop-types' +import React from 'react' +import fs from 'graceful-fs' + +const UploadDialog = ({source, path, actions}) => { + const onUploadClick = () => { + source.forEach((file) => { + if (fs.statSync(file).isDirectory()) { + actions.uploadFolder(path, file) + } else { + actions.uploadFile(path, file) + } + }) + actions.hideUploadDialog() + } + const onCancelClick = () => actions.hideUploadDialog() + return ( +
    +
    +

    Confirm Upload

    +
    Would you like to upload {source.length} {source.length === 1 ? 'item' : 'items'}?
    +
    + + +
    +
    +
    + ) +} + +UploadDialog.propTypes = { + source: PropTypes.array.isRequired, + path: PropTypes.string.isRequired, +} + +export default UploadDialog diff --git a/plugins/Inspector/js/components/uploadlist.js b/plugins/Inspector/js/components/uploadlist.js new file mode 100644 index 00000000..d4070477 --- /dev/null +++ b/plugins/Inspector/js/components/uploadlist.js @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types' +import React from 'react' +import TransferList from './transferlist.js' +import { List } from 'immutable' + +const UploadList = ({uploads, onUploadClick}) => ( +
    +

    Uploads

    + +
    +) + +UploadList.propTypes = { + uploads: PropTypes.instanceOf(List).isRequired, + onUploadClick: PropTypes.func, +} + +export default UploadList diff --git a/plugins/Inspector/js/components/usagestats.js b/plugins/Inspector/js/components/usagestats.js new file mode 100644 index 00000000..59b63bd6 --- /dev/null +++ b/plugins/Inspector/js/components/usagestats.js @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const UsageStats = ({allowance, spending}) => ( +
    + {spending} SC Spent / {allowance} SC Allocated +
    +) + +UsageStats.propTypes = { + allowance: PropTypes.string.isRequired, + spending: PropTypes.string.isRequired, +} + +export default UsageStats diff --git a/plugins/Inspector/js/constants/files.js b/plugins/Inspector/js/constants/files.js new file mode 100644 index 00000000..5e70f858 --- /dev/null +++ b/plugins/Inspector/js/constants/files.js @@ -0,0 +1,66 @@ +export const GET_WALLET_LOCKSTATE = 'GET_WALLET_LOCKSTATE' +export const RECEIVE_WALLET_LOCKSTATE = 'SET_WALLET_LOCKSTATE' +export const GET_STORAGE_ESTIMATE = 'GET_STORAGE_ESTIMATE' +export const SET_STORAGE_ESTIMATE = 'SET_STORAGE_ESTIMATE' +export const SET_FEE_ESTIMATE = 'SET_FEE_ESTIMATE' +export const GET_ALLOWANCE = 'GET_ALLOWANCE' +export const RECEIVE_ALLOWANCE = 'RECEIVE_ALLOWANCE' +export const RECEIVE_SPENDING = 'RECEIVE_SPENDING' +export const SET_ALLOWANCE = 'SET_ALLOWANCE' +export const SET_ALLOWANCE_COMPLETED = 'SET_ALLOWANCE_COMPLETED' +export const SET_PATH = 'SET_PATH' +export const GET_FILES = 'GET_FILES' +export const RECEIVE_FILES = 'RECEIVE_FILES' +export const GET_WALLET_BALANCE = 'GET_WALLET_BALANCE' +export const RECEIVE_WALLET_BALANCE = 'RECEIVE_WALLET_BALANCE' +export const SHOW_ALLOWANCE_DIALOG = 'SHOW_ALLOWANCE_DIALOG' +export const CLOSE_ALLOWANCE_DIALOG = 'CLOSE_ALLOWANCE_DIALOG' +export const SET_SEARCH_TEXT = 'SET_SEARCH_TEXT' +export const TOGGLE_SEARCH_FIELD = 'TOGGLE_SEARCH_FIELD' +export const UPLOAD_FILE = 'UPLOAD_FILE' +export const UPLOAD_FOLDER = 'UPLOAD_FOLDER' +export const DELETE_FILE = 'DELETE_FILE' +export const SHOW_DELETE_DIALOG = 'SHOW_DELETE_DIALOG' +export const HIDE_DELETE_DIALOG = 'HIDE_DELETE_DIALOG' +export const DOWNLOAD_FILE = 'DOWNLOAD_FILE' +export const SET_DRAGGING = 'SET_DRAGGING' +export const SET_NOT_DRAGGING = 'SET_NOT_DRAGGING' +export const SHOW_UPLOAD_DIALOG = 'SHOW_UPLOAD_DIALOG' +export const HIDE_UPLOAD_DIALOG = 'HIDE_UPLOAD_DIALOG' +export const GET_DOWNLOADS = 'GET_DOWNLOADS' +export const GET_UPLOADS = 'GET_UPLOADS' +export const RECEIVE_DOWNLOADS = 'RECEIVE_DOWNLOADS' +export const RECEIVE_UPLOADS = 'RECEIVE_UPLOADS' +export const CALCULATE_STORAGE_COST = 'CALCULATE_STORAGE_COST' +export const SHOW_FILE_TRANSFERS = 'SHOW_FILE_TRANSFERS' +export const HIDE_FILE_TRANSFERS = 'HIDE_FILE_TRANSFERS' +export const TOGGLE_FILE_TRANSFERS = 'TOGGLE_FILE_TRANSFERS' +export const SET_CONTRACT_COUNT = 'SET_CONTRACT_COUNT' +export const GET_CONTRACT_COUNT = 'GET_CONTRACT_COUNT' +export const RENAME_FILE = 'RENAME_FILE' +export const SHOW_RENAME_DIALOG = 'SHOW_RENAME_DIALOG' +export const HIDE_RENAME_DIALOG = 'HIDE_RENAME_DIALOG' +export const SELECT_FILE = 'SELECT_FILE' +export const SELECT_UP_TO = 'SELECT_UP_TO' +export const DESELECT_ALL = 'DESELECT_ALL' +export const DESELECT_FILE = 'DESELECT_FILE' +export const CLEAR_DOWNLOADS = 'CLEAR_DOWNLOADS' +export const SHOW_ALLOWANCE_CONFIRMATION = 'SHOW_ALLOWANCE_CONFIRMATION' +export const HIDE_ALLOWANCE_CONFIRMATION = 'HIDE_ALLOWANCE_CONFIRMATION' +export const GET_WALLET_SYNCSTATE = 'GET_WALLET_SYNCSTATE' +export const SET_WALLET_SYNCSTATE = 'SET_WALLET_SYNCSTATE' +export const FETCH_DATA = 'FETCH_DATA' +export const ADD_FOLDER = 'ADD_FOLDER' +export const SHOW_ADD_FOLDER_DIALOG = 'SHOW_ADD_FOLDER_DIALOG' +export const HIDE_ADD_FOLDER_DIALOG = 'HIDE_ADD_FOLDER_DIALOG' +export const SET_DRAG_UPLOAD_ENABLED = 'SET_DRAG_UPLOAD_ENABLED' +export const SET_DRAG_FOLDER_TARGET = 'SET_DRAG_FOLDER_TARGET' +export const SET_DRAG_FILE_ORIGIN = 'SET_DRAG_FILE_ORIGIN' +export const RENAME_SIA_UI_FOLDER = 'RENAME_SIA_UI_FOLDER' +export const DELETE_SIA_UI_FOLDER = 'DELETE_SIA_UI_FOLDER' + +export const SHOW_FILE_DETAIL = 'SHOW_FILE_DETAIL' +export const CLOSE_FILE_DETAIL = 'CLOSE_FILE_DETAIL' +export const GET_FILE_DETAIL = 'GET_FILE_DETAIL' +export const RECEIVE_FILE_DETAIL = 'RECEIVE_FILE_DETAIL' +export const DEFAULT_PAGE_SIZE = 200 diff --git a/plugins/Inspector/js/containers/addfolderbutton.js b/plugins/Inspector/js/containers/addfolderbutton.js new file mode 100644 index 00000000..33d052ca --- /dev/null +++ b/plugins/Inspector/js/containers/addfolderbutton.js @@ -0,0 +1,13 @@ +import AddFolderButtonView from '../components/addfolderbutton.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { showAddFolderDialog } from '../actions/files.js' + +const mapStateToProps = () => ({ +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ showAddFolderDialog }, dispatch), +}) + +const AddFolderButton = connect(mapStateToProps, mapDispatchToProps)(AddFolderButtonView) +export default AddFolderButton diff --git a/plugins/Inspector/js/containers/addfolderdialog.js b/plugins/Inspector/js/containers/addfolderdialog.js new file mode 100644 index 00000000..158b28f2 --- /dev/null +++ b/plugins/Inspector/js/containers/addfolderdialog.js @@ -0,0 +1,13 @@ +import AddFolderDialogView from '../components/addfolderdialog.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { hideAddFolderDialog, addFolder } from '../actions/files.js' + +const mapStateToProps = () => ({ +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ hideAddFolderDialog, addFolder}, dispatch), +}) + +const AddFolderDialog = connect(mapStateToProps, mapDispatchToProps)(AddFolderDialogView) +export default AddFolderDialog diff --git a/plugins/Inspector/js/containers/allowancedialog.js b/plugins/Inspector/js/containers/allowancedialog.js new file mode 100644 index 00000000..c634f04c --- /dev/null +++ b/plugins/Inspector/js/containers/allowancedialog.js @@ -0,0 +1,19 @@ +import AllowanceDialogView from '../components/allowancedialog.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { showAllowanceConfirmation, hideAllowanceConfirmation, closeAllowanceDialog, setAllowance, setFeeEstimate, getStorageEstimate } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + unlocked: state.wallet.get('unlocked'), + synced: state.wallet.get('synced'), + storageEstimate: state.allowancedialog.get('storageEstimate'), + feeEstimate: state.allowancedialog.get('feeEstimate'), + confirmationAllowance: state.allowancedialog.get('confirmationAllowance'), + confirming: state.allowancedialog.get('confirming'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ getStorageEstimate, setFeeEstimate, showAllowanceConfirmation, setAllowance, hideAllowanceConfirmation, closeAllowanceDialog}, dispatch), +}) + +const AllowanceDialog = connect(mapStateToProps, mapDispatchToProps)(AllowanceDialogView) +export default AllowanceDialog diff --git a/plugins/Inspector/js/containers/app.js b/plugins/Inspector/js/containers/app.js new file mode 100644 index 00000000..dde468fb --- /dev/null +++ b/plugins/Inspector/js/containers/app.js @@ -0,0 +1,9 @@ +import AppView from '../components/app.js' +import { connect } from 'react-redux' + +const mapStateToProps = (state) => ({ + showAllowanceDialog: state.files.get('showAllowanceDialog'), +}) + +const App = connect(mapStateToProps)(AppView) +export default App diff --git a/plugins/Inspector/js/containers/contractorstatus.js b/plugins/Inspector/js/containers/contractorstatus.js new file mode 100644 index 00000000..2cef4f7f --- /dev/null +++ b/plugins/Inspector/js/containers/contractorstatus.js @@ -0,0 +1,11 @@ +import ContractorStatusView from '../components/contractorstatus.js' +import { connect } from 'react-redux' + +const mapStateToProps = (state) => ({ + settingAllowance: state.files.get('settingAllowance'), + contractCount: state.files.get('contractCount'), +}) + +const ContractorStatus = connect(mapStateToProps)(ContractorStatusView) +export default ContractorStatus + diff --git a/plugins/Inspector/js/containers/deletedialog.js b/plugins/Inspector/js/containers/deletedialog.js new file mode 100644 index 00000000..53b4e4f4 --- /dev/null +++ b/plugins/Inspector/js/containers/deletedialog.js @@ -0,0 +1,14 @@ +import DeleteDialogView from '../components/deletedialog.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { hideDeleteDialog, deleteFile } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + files: state.deletedialog.get('files'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ hideDeleteDialog, deleteFile }, dispatch), +}) + +const DeleteDialog = connect(mapStateToProps, mapDispatchToProps)(DeleteDialogView) +export default DeleteDialog diff --git a/plugins/Inspector/js/containers/filebrowser.js b/plugins/Inspector/js/containers/filebrowser.js new file mode 100644 index 00000000..a3032fdf --- /dev/null +++ b/plugins/Inspector/js/containers/filebrowser.js @@ -0,0 +1,22 @@ +import FileBrowserView from '../components/filebrowser.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { setDragging, deselectAll, setNotDragging, showUploadDialog } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + dragging: state.files.get('dragging'), + settingAllowance: state.files.get('settingAllowance'), + showRenameDialog: state.files.get('showRenameDialog'), + showUploadDialog: state.files.get('showUploadDialog'), + showFileTransfers: state.files.get('showFileTransfers'), + showDeleteDialog: state.files.get('showDeleteDialog'), + showAddFolderDialog: state.files.get('showAddFolderDialog'), + dragUploadEnabled: state.files.get('dragUploadEnabled'), +}) + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ setDragging, deselectAll, setNotDragging, showUploadDialog }, dispatch), +}) + +const FileBrowser = connect(mapStateToProps, mapDispatchToProps)(FileBrowserView) +export default FileBrowser diff --git a/plugins/Inspector/js/containers/filecontrols.js b/plugins/Inspector/js/containers/filecontrols.js new file mode 100644 index 00000000..9f2a2743 --- /dev/null +++ b/plugins/Inspector/js/containers/filecontrols.js @@ -0,0 +1,14 @@ +import FileControlsView from '../components/filecontrols.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { downloadFile, showRenameDialog, showDeleteDialog } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + files: state.files.get('selected'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ downloadFile, showRenameDialog, showDeleteDialog }, dispatch), +}) + +const FileControls = connect(mapStateToProps, mapDispatchToProps)(FileControlsView) +export default FileControls diff --git a/plugins/Inspector/js/containers/filedetail.js b/plugins/Inspector/js/containers/filedetail.js new file mode 100644 index 00000000..26b12c70 --- /dev/null +++ b/plugins/Inspector/js/containers/filedetail.js @@ -0,0 +1,16 @@ +import FileDetailView from '../components/filedetail.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { showFileDetail, closeFileDetail, fetchFileDetail } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + showDetailPath: state.files.get('showDetailPath'), + showDetailFile: state.files.get('showDetailFile'), + current: state.files.get('current'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ showFileDetail, closeFileDetail, fetchFileDetail }, dispatch), +}) + +const FileDetail = connect(mapStateToProps, mapDispatchToProps)(FileDetailView) +export default FileDetail diff --git a/plugins/Inspector/js/containers/filelist.js b/plugins/Inspector/js/containers/filelist.js new file mode 100644 index 00000000..d8800990 --- /dev/null +++ b/plugins/Inspector/js/containers/filelist.js @@ -0,0 +1,22 @@ +import FileListView from '../components/filelist.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { renameSiaUIFolder, deleteSiaUIFolder, renameFile, getFiles, setDragFolderTarget, setDragFileOrigin, setDragUploadEnabled, setPath, selectUpTo, deselectFile, deselectAll, selectFile, downloadFile, showDeleteDialog, showRenameDialog, showFileDetail, closeFileDetail, fetchFileDetail } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + files: state.files.get('workingDirectoryFiles'), + selected: state.files.get('selected'), + searchResults: state.files.get('searchResults'), + path: state.files.get('path'), + showSearchField: state.files.get('showSearchField'), + dragFolderTarget: state.files.get('dragFolderTarget'), + dragFileOrigin: state.files.get('dragFileOrigin'), + showDetailPath: state.files.get('showDetailPath'), + showDetailFile: state.files.get('showDetailFile'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ renameSiaUIFolder, deleteSiaUIFolder, getFiles, renameFile, setDragFileOrigin, setDragFolderTarget, setDragUploadEnabled, selectUpTo, setPath, deselectFile, deselectAll, selectFile, showRenameDialog, downloadFile, showDeleteDialog, showFileDetail, closeFileDetail, fetchFileDetail }, dispatch), +}) + +const FileList = connect(mapStateToProps, mapDispatchToProps)(FileListView) +export default FileList diff --git a/plugins/Inspector/js/containers/filetransfers.js b/plugins/Inspector/js/containers/filetransfers.js new file mode 100644 index 00000000..93dc740a --- /dev/null +++ b/plugins/Inspector/js/containers/filetransfers.js @@ -0,0 +1,15 @@ +import FileTransfersView from '../components/filetransfers.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { getDownloads, clearDownloads, hideFileTransfers } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + uploads: state.files.get('uploading'), + downloads: state.files.get('downloading'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ getDownloads, clearDownloads, hideFileTransfers }, dispatch), +}) + +const FileTransfers = connect(mapStateToProps, mapDispatchToProps)(FileTransfersView) +export default FileTransfers diff --git a/plugins/Inspector/js/containers/renamedialog.js b/plugins/Inspector/js/containers/renamedialog.js new file mode 100644 index 00000000..f74eb832 --- /dev/null +++ b/plugins/Inspector/js/containers/renamedialog.js @@ -0,0 +1,14 @@ +import RenameDialogView from '../components/renamedialog.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { hideRenameDialog, renameFile } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + file: state.renamedialog.get('file'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ hideRenameDialog, renameFile }, dispatch), +}) + +const RenameDialog = connect(mapStateToProps, mapDispatchToProps)(RenameDialogView) +export default RenameDialog diff --git a/plugins/Inspector/js/containers/searchbutton.js b/plugins/Inspector/js/containers/searchbutton.js new file mode 100644 index 00000000..d3b2f90a --- /dev/null +++ b/plugins/Inspector/js/containers/searchbutton.js @@ -0,0 +1,14 @@ +import SearchButtonView from '../components/searchbutton.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { toggleSearchField, setSearchText } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + path: state.files.get('path'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ toggleSearchField, setSearchText }, dispatch), +}) + +const SearchButton = connect(mapStateToProps, mapDispatchToProps)(SearchButtonView) +export default SearchButton diff --git a/plugins/Inspector/js/containers/searchfield.js b/plugins/Inspector/js/containers/searchfield.js new file mode 100644 index 00000000..58935857 --- /dev/null +++ b/plugins/Inspector/js/containers/searchfield.js @@ -0,0 +1,15 @@ +import SearchFieldView from '../components/searchfield.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { setSearchText } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + searchText: state.files.get('searchText'), + path: state.files.get('path'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ setSearchText }, dispatch), +}) + +const SearchField = connect(mapStateToProps, mapDispatchToProps)(SearchFieldView) +export default SearchField diff --git a/plugins/Inspector/js/containers/setallowancebutton.js b/plugins/Inspector/js/containers/setallowancebutton.js new file mode 100644 index 00000000..a48dff8d --- /dev/null +++ b/plugins/Inspector/js/containers/setallowancebutton.js @@ -0,0 +1,13 @@ +import SetAllowanceButtonView from '../components/setallowancebutton.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { showAllowanceDialog } from '../actions/files.js' + +const mapStateToProps = () => ({ +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ showAllowanceDialog }, dispatch), +}) + +const SetAllowanceButton = connect(mapStateToProps, mapDispatchToProps)(SetAllowanceButtonView) +export default SetAllowanceButton diff --git a/plugins/Inspector/js/containers/transfersbutton.js b/plugins/Inspector/js/containers/transfersbutton.js new file mode 100644 index 00000000..d7af509c --- /dev/null +++ b/plugins/Inspector/js/containers/transfersbutton.js @@ -0,0 +1,14 @@ +import TransfersButtonView from '../components/transfersbutton.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { toggleFileTransfers } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + unread: state.files.get('unreadUploads').size + state.files.get('unreadDownloads').size, +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ toggleFileTransfers }, dispatch), +}) + +const TransfersButton = connect(mapStateToProps, mapDispatchToProps)(TransfersButtonView) +export default TransfersButton diff --git a/plugins/Inspector/js/containers/uploadbutton.js b/plugins/Inspector/js/containers/uploadbutton.js new file mode 100644 index 00000000..59df9a1e --- /dev/null +++ b/plugins/Inspector/js/containers/uploadbutton.js @@ -0,0 +1,14 @@ +import UploadButtonView from '../components/uploadbutton.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { showUploadDialog } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + contracts: state.files.get('contractCount'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ showUploadDialog }, dispatch), +}) + +const UploadButton = connect(mapStateToProps, mapDispatchToProps)(UploadButtonView) +export default UploadButton diff --git a/plugins/Inspector/js/containers/uploaddialog.js b/plugins/Inspector/js/containers/uploaddialog.js new file mode 100644 index 00000000..a122cc86 --- /dev/null +++ b/plugins/Inspector/js/containers/uploaddialog.js @@ -0,0 +1,15 @@ +import UploadDialogView from '../components/uploaddialog.js' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { hideUploadDialog, uploadFile, uploadFolder } from '../actions/files.js' + +const mapStateToProps = (state) => ({ + source: state.files.get('uploadSource'), + path: state.files.get('path'), +}) +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ hideUploadDialog, uploadFile, uploadFolder }, dispatch), +}) + +const UploadDialog = connect(mapStateToProps, mapDispatchToProps)(UploadDialogView) +export default UploadDialog diff --git a/plugins/Inspector/js/containers/usagestats.js b/plugins/Inspector/js/containers/usagestats.js new file mode 100644 index 00000000..18c202ac --- /dev/null +++ b/plugins/Inspector/js/containers/usagestats.js @@ -0,0 +1,10 @@ +import UsageStatsView from '../components/usagestats.js' +import { connect } from 'react-redux' + +const mapStateToProps = (state) => ({ + allowance: state.files.get('allowance'), + spending: state.files.get('spending'), +}) + +const UsageStats = connect(mapStateToProps)(UsageStatsView) +export default UsageStats diff --git a/plugins/Inspector/js/index.js b/plugins/Inspector/js/index.js new file mode 100644 index 00000000..f5abdb45 --- /dev/null +++ b/plugins/Inspector/js/index.js @@ -0,0 +1,28 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import createSagaMiddleware from 'redux-saga' +import { createStore, applyMiddleware } from 'redux' +import { Provider } from 'react-redux' +import rootReducer from './reducers/index.js' +import rootSaga from './sagas/index.js' +import App from './containers/app.js' +import { fetchData } from './actions/files.js' + +const sagaMiddleware = createSagaMiddleware() +const store = createStore( + rootReducer, + applyMiddleware(sagaMiddleware) +) +sagaMiddleware.run(rootSaga) + +const rootElement = ( + + + +) +ReactDOM.render(rootElement, document.getElementById('react-root')) + +// update state when plugin is focused +window.onfocus = () => { + store.dispatch(fetchData()) +} diff --git a/plugins/Inspector/js/reducers/allowancedialog.js b/plugins/Inspector/js/reducers/allowancedialog.js new file mode 100644 index 00000000..99382403 --- /dev/null +++ b/plugins/Inspector/js/reducers/allowancedialog.js @@ -0,0 +1,28 @@ +import { Map } from 'immutable' +import * as constants from '../constants/files.js' + +const initialState = Map({ + storageEstimate: '0 B', + feeEstimate: 0, + confirming: false, + confirmationAllowance: '0', +}) + +export default function allowancedialogReduceR(state = initialState, action) { + switch (action.type) { + case constants.SHOW_ALLOWANCE_CONFIRMATION: + return state.set('confirming', true) + .set('confirmationAllowance', action.allowance) + case constants.HIDE_ALLOWANCE_CONFIRMATION: + return state.set('confirming', false) + case constants.CLOSE_ALLOWANCE_DIALOG: + return state.set('confirming', false) + case constants.SET_FEE_ESTIMATE: + return state.set('feeEstimate', action.estimate) + case constants.SET_STORAGE_ESTIMATE: + return state.set('storageEstimate', action.estimate) + default: + return state + } +} + diff --git a/plugins/Inspector/js/reducers/deletedialog.js b/plugins/Inspector/js/reducers/deletedialog.js new file mode 100644 index 00000000..0a4d61c9 --- /dev/null +++ b/plugins/Inspector/js/reducers/deletedialog.js @@ -0,0 +1,15 @@ +import { Map, List } from 'immutable' +import * as constants from '../constants/files.js' + +const initialState = Map({ + files: List(), +}) + +export default function deletedialogReducer(state = initialState, action) { + switch (action.type) { + case constants.SHOW_DELETE_DIALOG: + return state.set('files', action.files) + default: + return state + } +} diff --git a/plugins/Inspector/js/reducers/files.js b/plugins/Inspector/js/reducers/files.js new file mode 100644 index 00000000..c35274df --- /dev/null +++ b/plugins/Inspector/js/reducers/files.js @@ -0,0 +1,174 @@ +import { Map, Set, OrderedSet, List } from 'immutable' +import * as constants from '../constants/files.js' +import { ls, searchFiles, allFiles, rangeSelect } from '../sagas/helpers.js' +import Path from 'path' + +const initialState = Map({ + files: List(), + folders: List(), + workingDirectoryFiles: null, + searchResults: List(), + uploading: List(), + downloading: List(), + selected: OrderedSet(), + path: '', + searchText: '', + uploadSource: '', + showAllowanceDialog: false, + showAddFolderDialog: false, + showUploadDialog: false, + showSearchField: false, + showFileTransfers: false, + showDeleteDialog: false, + showRenameDialog: false, + settingAllowance: false, + dragging: false, + dragUploadEnabled: true, + dragFolderTarget: '', + dragFileOrigin: {}, + contractCount: 0, + allowance: '0', + spending: '0', + showDownloadsSince: Date.now(), + unreadUploads: Set(), + unreadDownloads: Set(), + showDetailPath: null, +}) + + +export default function filesReducer(state = initialState, action) { + switch (action.type) { + case constants.SET_DRAG_FILE_ORIGIN: + return state.set('dragFileOrigin', action.origin) + case constants.SET_DRAG_FOLDER_TARGET: + return state.set('dragFolderTarget', action.target) + case constants.SET_DRAG_UPLOAD_ENABLED: + return state.set('dragUploadEnabled', action.enabled) + case constants.SET_ALLOWANCE_COMPLETED: + return state.set('settingAllowance', false) + case constants.RECEIVE_ALLOWANCE: + return state.set('allowance', action.allowance) + case constants.RECEIVE_SPENDING: + return state.set('spending', action.spending) + case constants.DOWNLOAD_FILE: + return state.set('unreadDownloads', state.get('unreadDownloads').add(action.file.siapath)) + case constants.UPLOAD_FILE: + return state.set('unreadUploads', state.get('unreadUploads').add(action.siapath)) + case constants.RECEIVE_FILES: { + const workingDirectoryFiles = ls(action.files.concat(state.get('folders')), state.get('path')) + const workingDirectorySiapaths = workingDirectoryFiles.map((file) => file.siapath) + // filter out selected files that are no longer in the working directory + const selected = state.get('selected').filter((file) => workingDirectorySiapaths.includes(file.siapath)) + return state.set('files', action.files) + .set('workingDirectoryFiles', workingDirectoryFiles) + .set('selected', selected) + } + case constants.ADD_FOLDER: { + const folder = { + filesize: 0, + siapath: Path.join(state.get('path'), action.name), + available: false, + redundancy: -1, + uploadprogress: 100, + siaUIFolder: true, + } + const folders = state.get('folders').push(folder) + return state.set('folders', folders) + .set('workingDirectoryFiles', ls(state.get('files').concat(folders), state.get('path')), folders, state.get('path')) + } + case constants.DELETE_SIA_UI_FOLDER: { + return state.set('folders', state.get('folders').filter((folder) => { + const cleanSiapath = action.siapath.replace(/\/$/, '') + const cleanSource = folder.siapath.replace(/\/$/, '') + return cleanSiapath !== cleanSource + })) + } + case constants.RENAME_SIA_UI_FOLDER: + return state.set('folders', state.get('folders').map((folder) => { + const cleanSource = action.source.replace(/\/$/, '') + if (folder.siapath.indexOf(cleanSource) === 0) { + folder.siapath = action.dest + } + return folder + })) + case constants.SET_ALLOWANCE: + return state.set('allowance', action.funds) + .set('settingAllowance', true) + case constants.CLEAR_DOWNLOADS: + return state.set('showDownloadsSince', Date.now()) + case constants.SET_SEARCH_TEXT: { + const results = searchFiles(allFiles(state), action.text, state.get('path')) + return state.set('searchResults', results) + .set('searchText', action.text) + } + case constants.SET_PATH: { + const workingDirFiles = ls(allFiles(state), action.path) + return state.set('path', action.path) + .set('selected', OrderedSet()) + .set('workingDirectoryFiles', workingDirFiles) + .set('searchResults', searchFiles(allFiles(state), state.get('searchText'), action.path)) + } + case constants.DESELECT_FILE: + return state.set('selected', state.get('selected').filter((file) => file.siapath !== action.file.siapath)) + case constants.SELECT_FILE: + return state.set('selected', state.get('selected').add(action.file)) + case constants.DESELECT_ALL: + return state.set('selected', OrderedSet()) + case constants.SELECT_UP_TO: + return state.set('selected', rangeSelect(action.file, state.get('workingDirectoryFiles'), state.get('selected'))) + case constants.SHOW_ALLOWANCE_DIALOG: + return state.set('showAllowanceDialog', true) + case constants.CLOSE_ALLOWANCE_DIALOG: + return state.set('showAllowanceDialog', false) + case constants.TOGGLE_SEARCH_FIELD: + return state.set('showSearchField', !state.get('showSearchField')) + case constants.SET_DRAGGING: + return state.set('dragging', true) + case constants.SET_NOT_DRAGGING: + return state.set('dragging', false) + case constants.SHOW_DELETE_DIALOG: + return state.set('showDeleteDialog', true) + case constants.HIDE_DELETE_DIALOG: + return state.set('showDeleteDialog', false) + case constants.SHOW_UPLOAD_DIALOG: + return state.set('showUploadDialog', true) + .set('uploadSource', action.source) + case constants.HIDE_UPLOAD_DIALOG: + return state.set('showUploadDialog', false) + case constants.RECEIVE_UPLOADS: + return state.set('uploading', action.uploads) + case constants.RECEIVE_DOWNLOADS: + return state.set('downloading', action.downloads.filter((download) => Date.parse(download.starttime) > state.get('showDownloadsSince'))) + case constants.SHOW_FILE_TRANSFERS: + return state.set('showFileTransfers', true) + case constants.HIDE_FILE_TRANSFERS: + return state.set('showFileTransfers', false) + case constants.TOGGLE_FILE_TRANSFERS: + return state.set('showFileTransfers', !state.get('showFileTransfers')) + .set('unreadDownloads', Set()) + .set('unreadUploads', Set()) + case constants.SET_CONTRACT_COUNT: + return state.set('contractCount', action.count) + case constants.SHOW_RENAME_DIALOG: + return state.set('showRenameDialog', true) + case constants.HIDE_RENAME_DIALOG: + return state.set('showRenameDialog', false) + case constants.SHOW_ADD_FOLDER_DIALOG: + return state.set('showAddFolderDialog', true) + case constants.HIDE_ADD_FOLDER_DIALOG: + return state.set('showAddFolderDialog', false) + case constants.SHOW_FILE_DETAIL: + return state.set('showDetailPath', action.siapath) + case constants.RECEIVE_FILE_DETAIL: + return state.set('pagingNum', action.pagingNum) + .set('current', action.current) + .set('showDetailFile', action.file) + case constants.CLOSE_FILE_DETAIL: + return state.delete('showDetailPath') + .delete('showDetailFile') + .delete('pagingNum') + .delete('current') + default: + return state + } +} diff --git a/plugins/Inspector/js/reducers/index.js b/plugins/Inspector/js/reducers/index.js new file mode 100644 index 00000000..c68892d3 --- /dev/null +++ b/plugins/Inspector/js/reducers/index.js @@ -0,0 +1,16 @@ +import { combineReducers } from 'redux' +import wallet from './wallet.js' +import files from './files.js' +import deletedialog from './deletedialog.js' +import renamedialog from './renamedialog.js' +import allowancedialog from './allowancedialog.js' + +const rootReducer = combineReducers({ + wallet, + files, + deletedialog, + renamedialog, + allowancedialog, +}) + +export default rootReducer diff --git a/plugins/Inspector/js/reducers/renamedialog.js b/plugins/Inspector/js/reducers/renamedialog.js new file mode 100644 index 00000000..8ef997a6 --- /dev/null +++ b/plugins/Inspector/js/reducers/renamedialog.js @@ -0,0 +1,15 @@ +import { Map } from 'immutable' +import * as constants from '../constants/files.js' + +const initialState = Map({ + file: {}, +}) + +export default function renamedialogReducer(state = initialState, action) { + switch (action.type) { + case constants.SHOW_RENAME_DIALOG: + return state.set('file', action.file) + default: + return state + } +} diff --git a/plugins/Inspector/js/reducers/wallet.js b/plugins/Inspector/js/reducers/wallet.js new file mode 100644 index 00000000..d468fa8f --- /dev/null +++ b/plugins/Inspector/js/reducers/wallet.js @@ -0,0 +1,21 @@ +import { Map } from 'immutable' +import * as constants from '../constants/files.js' + +const initialState = Map({ + unlocked: false, + synced: false, + balance: '', +}) + +export default function walletReducer(state = initialState, action) { + switch (action.type) { + case constants.RECEIVE_WALLET_LOCKSTATE: + return state.set('unlocked', action.unlocked) + case constants.RECEIVE_WALLET_BALANCE: + return state.set('balance', action.balance) + case constants.SET_WALLET_SYNCSTATE: + return state.set('synced', action.synced) + default: + return state + } +} diff --git a/plugins/Inspector/js/sagas/files.js b/plugins/Inspector/js/sagas/files.js new file mode 100644 index 00000000..809d1dbd --- /dev/null +++ b/plugins/Inspector/js/sagas/files.js @@ -0,0 +1,62 @@ +import { takeEvery, delay } from 'redux-saga' +import { fork, join, put, race, take, call, select } from 'redux-saga/effects' +import * as actions from '../actions/files.js' +import * as constants from '../constants/files.js' +import { List } from 'immutable' +import { siadCall } from './helpers.js' + +// Query siad for the user's files. +function* getFilesSaga() { + try { + const response = yield siadCall('/renter/files') + const files = List(response.files) + yield put(actions.receiveFiles(files)) + } catch (e) { + console.error('error fetching files: ' + e.toString()) + } +} + +function* getFileDetailSaga(action) { + let siapath, pagingNum, current + + if (action) { + ({ siapath, pagingNum, current } = action) + } else { + ({ siapath, pagingNum, current } = yield select((state) => ({ + siapath: state.files.get('showDetailPath'), pagingNum: state.files.get('pagingNum'), current: state.files.get('current'), + }))) + if (!siapath) { + return + } + } + + try { + const response = yield siadCall('/renter/filedetail/' + + encodeURI(siapath) + + '?pagingNum=' + pagingNum + + '¤t=' + current) + yield put(actions.receiveFileDetail({ pagingNum, current, file: response })) + } catch (e) { + console.error('error fetching file: ' + e.toString()) + } +} + +export function* dataFetcher() { + while (true) { + let tasks = [] + tasks = tasks.concat(yield fork(getFilesSaga)) + tasks = tasks.concat(yield fork(getFileDetailSaga)) + + yield join(...tasks) + yield race({ + task: call(delay, 8000), + cancel: take(constants.FETCH_DATA), + }) + } +} +export function* watchGetFiles() { + yield *takeEvery(constants.GET_FILES, getFilesSaga) +} +export function* watchGetFileDetail() { + yield *takeEvery(constants.GET_FILE_DETAIL, getFileDetailSaga) +} diff --git a/plugins/Inspector/js/sagas/helpers.js b/plugins/Inspector/js/sagas/helpers.js new file mode 100644 index 00000000..6f7d1ee6 --- /dev/null +++ b/plugins/Inspector/js/sagas/helpers.js @@ -0,0 +1,271 @@ +// Helper functions for the Files sagas. +import { List, Map } from 'immutable' +import Path from 'path' +import fs from 'graceful-fs' +import * as actions from '../actions/files.js' + +export const blockMonth = 4320 +export const allowanceMonths = 3 +export const allowancePeriod = blockMonth*allowanceMonths +export const ncontracts = 24 +export const baseRedundancy = 6 +export const baseFee = 240 +export const siafundRate = 0.12 + +// sendError sends the error given by e to the ui for display. +export const sendError = (e) => { + SiaAPI.showError({ + title: 'Sia-UI Files Error', + content: typeof e.message !== 'undefined' ? e.message : e.toString(), + }) +} + +// siadCall: promisify Siad API calls. Resolve the promise with `response` if the call was successful, +// otherwise reject the promise with `err`. +export const siadCall = (uri) => new Promise((resolve, reject) => { + SiaAPI.call(uri, (err, response) => { + if (err) { + reject(err) + } else { + resolve(response) + } + }) +}) + +// Take a number of bytes and return a sane, human-readable size. +export const readableFilesize = (bytes) => { + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + let readableunit = 'B' + let readablesize = bytes + for (const unit in units) { + if (readablesize < 1000) { + readableunit = units[unit] + break + } + readablesize /= 1000 + } + return readablesize.toFixed().toString() + ' ' + readableunit +} + +// minRedundancy takes a list of files and returns the minimum redundancy that +// occurs in the list. +export const minRedundancy = (files) => { + if (files.size === 0) { + return 0 + } + const redundantFiles = files.filter((file) => file.redundancy >= 0) + + // if all the provided files have -1 redundancy, return -1. + if (redundantFiles.size === 0) { + return -1 + } + + // return the minimum redundancy of all the files with redundancy >= 0 + return redundantFiles.min((a, b) => { + if (a.redundancy > b.redundancy) { + return 1 + } + return -1 + }).redundancy +} + +// minUpload takes a list of files and returns the minimum upload progress that +// occurs in the list. +export const minUpload = (files) => { + if (files.size === 0) { + return 0 + } + + return files.map((f) => f.uploadprogress).min() +} +// directoriesFirst is a comparator function used to sort files by type, where +// the directories will always come first. +const directoriesFirst = (file1, file2) => { + if (file1.type === 'directory' && file2.type === 'file') { + return -1 + } + if (file1.type === 'file' && file2.type === 'directory') { + return 1 + } + return 0 +} + + +// return a list of files filtered with path. +// ... it's ls. +export const ls = (files, path) => { + const fileList = files.filter((file) => file.siapath.includes(path) && file.siapath !== path) + let parsedFiles = Map() + fileList.forEach((file) => { + let type = 'file' + const relativePath = Path.posix.relative(path, file.siapath) + let filename = Path.posix.basename(relativePath) + let uploadprogress = Math.floor(file.uploadprogress) + let siapath = file.siapath + let filesize = readableFilesize(file.filesize) + let redundancy = file.redundancy + if (relativePath.indexOf('/') !== -1 || file.siaUIFolder === true) { + type = 'directory' + filename = relativePath.split('/')[0] + } + if (parsedFiles.has(filename) && parsedFiles.get(filename).type === type) { + return + } + if (type === 'directory') { + // directories cannot be named '..'. + if (filename === '..') { + return + } + + siapath = Path.posix.join(path, filename) + '/' + const subfiles = files.filter((subfile) => subfile.siapath.includes(siapath)) + const totalFilesize = subfiles.reduce((sum, subfile) => sum + subfile.filesize, 0) + filesize = readableFilesize(totalFilesize) + if (!file.siaUIFolder) { + redundancy = minRedundancy(subfiles) + } else { + redundancy = -1 + } + uploadprogress = minUpload(subfiles) + } + parsedFiles = parsedFiles.set(filename, { + size: filesize, + name: filename, + siapath: siapath, + available: file.available, + redundancy: redundancy, + uploadprogress: uploadprogress, + siaUIFolder: file.siaUIFolder === true, + type, + details: file.details, + }) + }) + return parsedFiles.toList().sortBy((file) => file.name).sort(directoriesFirst) +} + +// recursive version of readdir +export const readdirRecursive = (path, files) => { + const dirfiles = fs.readdirSync(path) + let filelist + if (typeof files === 'undefined') { + filelist = List() + } else { + filelist = files + } + dirfiles.forEach((file) => { + const filepath = Path.join(path, file) + const stat = fs.statSync(filepath) + if (stat.isDirectory()) { + filelist = readdirRecursive(filepath, filelist) + } else if (stat.isFile()) { + filelist = filelist.push(filepath) + } + }) + return filelist +} + +// uploadDirectory takes a `directory`, a list of files inside the directory, +// and a destination siapath and returns a List of upload actions that will +// upload each file to `destpath/directoryname/`. +export const uploadDirectory = (directory, files, destpath) => + files.map((file) => { + const relativePath = Path.dirname(file.substring(directory.length + 1)) + const siapath = Path.posix.join(destpath, Path.basename(directory), relativePath) + return actions.uploadFile(siapath, file) + }) + +// Parse a response from `/renter/downloads` +// return a list of file downloads +export const parseDownloads = (downloads) => + List(downloads) + .map((download) => ({ + status: (() => { + if (Math.floor(download.received / download.filesize) === 1) { + return 'Completed' + } + return 'Downloading' + })(), + siapath: download.siapath, + name: Path.basename(download.siapath), + progress: Math.floor((download.received / download.filesize) * 100), + destination: download.destination, + type: 'download', + starttime: download.starttime, + })) + .sortBy((download) => -download.starttime) + +// Parse a list of files and return the total filesize +export const totalUsage = (files) => readableFilesize(files.reduce((sum, file) => sum + file.filesize, 0)) + +// Parse a list of files from `/renter/files` +// return a list of file uploads +export const parseUploads = (files) => + List(files) + .filter((file) => file.redundancy >= 0) + .filter((file) => file.uploadprogress < 100) + .map((upload) => ({ + status: (() => { + if (upload.redundancy < 1.0) { + return 'Uploading' + } + return 'Boosting Redundancy' + })(), + siapath: upload.siapath, + name: Path.basename(upload.siapath), + progress: Math.floor(upload.uploadprogress), + type: 'upload', + })) + .sortBy((upload) => upload.name) + .sortBy((upload) => -upload.progress) + +// Search `files` for `text`, excluding directories not in `path` +export const searchFiles = (files, text, path) => { + const filteredFiles = List(files) + .filter((file) => file.siapath.indexOf(path) === 0 && file.siapath !== path) + .filter((file) => file.siapath.toLowerCase().includes(text.toLowerCase())) + + let parsedFiles = Map() + filteredFiles.forEach((file) => { + let type = 'file' + let name = Path.posix.basename(file.siapath) + let siapath = file.siapath + const pathComponents = file.siapath.split('/') + if (!Path.posix.basename(file.siapath).toLowerCase().includes(text.toLowerCase()) || file.siaUIFolder) { + type = 'directory' + pathComponents.forEach((component, idx) => { + if (component.toLowerCase().includes(text.toLowerCase())) { + name = component + siapath = pathComponents.slice(0, idx+1).join('/') + '/' + } + }) + } + if (!parsedFiles.has(siapath) && siapath !== path) { + const parsedFile = Object.assign({}, file) + parsedFile.siapath = siapath + parsedFile.type = type + parsedFile.name = name + parsedFiles = parsedFiles.set(siapath, parsedFile) + } + }) + + return parsedFiles.toList() +} + +// rangeSelect takes a file to select, a list of files, and a set of selected +// files and returns a new set of selected files consisting of all the files +// between the last selected file and the clicked `file`. +export const rangeSelect = (file, files, selectedFiles) => { + const siapaths = files.map((f) => f.siapath) + const selectedSiapaths = selectedFiles.map((selectedfile) => selectedfile.siapath) + + const endSelectionIndex = siapaths.indexOf(file.siapath) + const startSelectionIndex = siapaths.indexOf(selectedSiapaths.first()) + if (startSelectionIndex > endSelectionIndex) { + return files.slice(endSelectionIndex, startSelectionIndex + 1).toOrderedSet().reverse() + } + return files.slice(startSelectionIndex, endSelectionIndex + 1).toOrderedSet() +} + + +// allFiles returns all the files in the state, including Sia-UI folders +export const allFiles = (state) => state.get('files').concat(state.get('folders')) diff --git a/plugins/Inspector/js/sagas/index.js b/plugins/Inspector/js/sagas/index.js new file mode 100644 index 00000000..8a61e030 --- /dev/null +++ b/plugins/Inspector/js/sagas/index.js @@ -0,0 +1,7 @@ +import * as sagas from './files.js' +import { fork } from 'redux-saga/effects' + +export default function* rootSaga() { + const watchers = Object.values(sagas).map(fork) + yield watchers +} diff --git a/test/plugins.unit.js b/test/plugins.unit.js index fc1fcd26..358f2156 100644 --- a/test/plugins.unit.js +++ b/test/plugins.unit.js @@ -3,7 +3,7 @@ import { getPluginName, getOrderedPlugins } from '../js/rendererjs/plugins.js' import { expect } from 'chai' const pluginDir = Path.join(__dirname, '../plugins') -const nPlugins = 6 +const nPlugins = 7 describe('plugin system', () => { describe('getOrderedPlugins', () => {