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.
+
+
+ Confirm
+ Cancel
+
+
+ )
+}
+
+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.
+
+
+
+ )
+
+ 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'}
+
+
+ Yes
+ No
+
+
+
+ )
+}
+
+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
+
+ Clear 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 (
+
+ )
+ }
+
+ 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}) => (
+
+
+
+)
+
+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 (
+
+ )
+}
+
+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.
+
+ OK
+
+
+)
+
+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'}?
+
+ Upload
+ Cancel
+
+
+
+ )
+}
+
+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', () => {