Compare commits

...

72 Commits
v1 ... master

Author SHA1 Message Date
9fbbf821f0 Rework project and also include checking monitoring 2025-05-09 22:00:00 +02:00
d51b815b92 Remove unused block 2025-05-07 18:45:36 +02:00
Michael Reber
ab721b9a7a fix minor stuff 2025-05-07 08:51:07 +02:00
Michael Reber
bc37b903b3 When mounts or remotes are empty hide them on UI 2025-05-06 19:10:35 +02:00
Michael Reber
f49ff032d4 Remove the non-working filebrowser 2025-05-06 18:44:44 +02:00
Michael Reber
7477319a68 fix bug that makes the dashboard blank 2025-05-06 18:12:30 +02:00
Michael Reber
ecd56b9ced prepare Upgrade 2025-05-06 17:54:28 +02:00
Luc Appelman
f393c767d3
forgot the html selector 2021-11-22 13:31:35 +01:00
Luc Appelman
6facf41dba
load colorscheme initially from css
Provides a faster way of knowing the default color scheme of the user. But will be overriden by the settings in settings.jsx
2021-11-22 13:29:46 +01:00
controlol
8d0361cd80 loader should always be the top element 2021-11-05 00:14:00 +01:00
controlol
b72efdeeda show loading during file action 2021-11-05 00:13:37 +01:00
controlol
c8a52981e6 button needs onclick as it's actually a div 2021-11-05 00:08:22 +01:00
controlol
c16b64bbf7 improve filemenu styling
add confirm popups
2021-11-05 00:01:41 +01:00
controlol
e0c3d48b01 move filemenu to filebrowsermenu class
create functions to execute after a action has been confirmed
2021-11-05 00:01:23 +01:00
controlol
60943ca082 create FileMenu class 2021-11-05 00:00:18 +01:00
controlol
431be1bf5a Merge branch 'master' of https://github.com/controlol/rclone-webui 2021-11-04 00:56:54 +01:00
controlol
1e8416046e fix open browser button 2021-11-04 00:56:49 +01:00
controlol
09684200e4 fix open browser button 2021-11-04 00:50:34 +01:00
controlol
f5c40d54ff show total speed of the job 2021-11-04 00:50:19 +01:00
controlol
41f15ae295 add file browser navigation button 2021-11-04 00:32:05 +01:00
controlol
243967259c Closes #15 added file action menu
not all buttons implemented yet
2021-11-04 00:31:52 +01:00
controlol
81e0d5fbdc highlight active remote 2021-11-03 22:49:37 +01:00
controlol
8d7375af4f get info after api recovers 2021-11-03 22:32:05 +01:00
controlol
e15f23b535 log message only and reject with message 2021-11-03 22:27:20 +01:00
controlol
ebbd030e84 Closes #11 Secondary browser is not function for mobile clients 2021-11-03 21:59:04 +01:00
controlol
54b9dbea79 Merge branch 'master' of https://github.com/controlol/rclone-webui 2021-11-03 21:17:36 +01:00
controlol
b7f609ecbb updatePath can only be called by a dir now 2021-11-03 21:17:08 +01:00
controlol
f9b91cc7ca disable clicks when a menu is opened 2021-11-03 21:17:08 +01:00
controlol
c7a4d3413f Closes #21 move filesettings one class up
fix issues with the path when opening second browser
2021-11-03 21:16:34 +01:00
controlol
c38d37e0c8 updatePath can only be called by a dir now 2021-11-03 21:13:15 +01:00
controlol
cb65ed446f disable clicks when a menu is opened 2021-11-03 21:13:15 +01:00
controlol
db429cf51f move filesettings one class up
fix issues with the path when opening second browser
2021-11-03 21:13:10 +01:00
controlol
18b20c2c60 clicking on a remote now changes the remote of the active browser 2021-11-03 19:34:57 +01:00
controlol
714381d24c improve icons
add some styling to visualize the active browser
clicking on the browser will set the active browser
2021-11-03 19:31:09 +01:00
controlol
a070bcf32c add dual browser functionality 2021-11-03 19:30:07 +01:00
controlol
cba4a29f36 add tertiary rclone color 2021-11-03 19:27:14 +01:00
controlol
17e4bee903 improve column settings popup styling
actually show date now instead of datetime
2021-11-02 23:44:43 +01:00
controlol
250ca3d1f8 correct filebrowser height for mobile view 2021-11-02 23:37:44 +01:00
controlol
0e0129acb5 Closes #18 with much better mobile experience
improves #20 by removing some scrolling/optimistic row bugs
2021-11-02 23:34:11 +01:00
controlol
d70cf64e68 Closes #20 add filebrowser settings menu
this removes old media queries to automatically show and hide columns
2021-11-02 22:49:51 +01:00
controlol
70492274df do not show tooltip on a click 2021-11-02 22:46:41 +01:00
controlol
b5f42a7a28 add modtime column 2021-11-02 21:07:49 +01:00
Luc Appelman
322af54ec6
add milestone badge 2021-11-02 08:05:28 +01:00
controlol
429982bb9b add comments
remove logging
2021-11-01 23:56:22 +01:00
controlol
a1b92f5587 Closes #19 only render visable gridrows
Doing this greatly improves render speed and bypasses the webbrowsers grid limitation of 1000 files
With a padding for a better user experience
2021-11-01 23:46:03 +01:00
controlol
dbd7956491 fix filebrowser hight and improve search speed
Used settimeout to prevent searching on every keystroke
2021-11-01 22:26:31 +01:00
controlol
dfc83360e5 loop and improve animations 2021-11-01 21:28:27 +01:00
controlol
320d2666ec fix clicking the from the path bar 2021-11-01 21:28:18 +01:00
controlol
613817fc46 add scrollbar styling 2021-11-01 21:27:57 +01:00
controlol
9f2264b1f4 fix filebrowser height 2021-11-01 21:27:47 +01:00
controlol
116ecdfb72 add loading state 2021-11-01 20:30:41 +01:00
controlol
1b032ec4c7 eventlistener should be created and deleted onMount and willUnmount
how to fuck up performance; componentDidUpdate
especially terrible during development when the component will be updated even more often
2021-11-01 20:29:58 +01:00
controlol
b50a91b6b5 add loading animation 2021-11-01 20:28:30 +01:00
controlol
c61fb12463 add mkv filetype and use switch 2021-11-01 19:18:15 +01:00
Luc
2e55399f53 no spaces around filename 2021-11-01 14:30:33 +01:00
Luc
0cdd5d2c79 rename action member and arguments 2021-11-01 12:07:05 +01:00
Luc
8f0442876c add action menu 2021-11-01 11:09:35 +01:00
Luc
a3ece750f3 display 0 if undefined 2021-11-01 09:52:09 +01:00
Luc
4b9e09673e add style for choosing remotefs
add wrapping to prepare for dual browser
2021-11-01 09:43:20 +01:00
Luc
41c4536d40 add assert 2021-11-01 09:42:18 +01:00
Luc
a15bb2bcc8 improve popup style 2021-11-01 09:41:26 +01:00
Luc
7f6216f032 add choose remotefs
rename browser variable to brIndex
add assert
2021-11-01 09:41:09 +01:00
Luc
423df7cfb1 filebrowser works with theme color 2021-11-01 09:29:01 +01:00
Luc
475b8d0845 show 0 instead of undefined
add remotes as prop to FileBrowserMenu
2021-11-01 09:20:36 +01:00
controlol
b9b69f349e Merge branch 'master' of https://github.com/controlol/rclone-webui 2021-10-31 21:51:17 +01:00
controlol
1fa2a994b7 add basic filebrowser functionality 2021-10-31 21:49:25 +01:00
controlol
ce81c77c88 errorCountString is not a function 2021-10-31 21:48:53 +01:00
controlol
9d1dbc35f6 improve popup styling 2021-10-31 21:48:26 +01:00
controlol
9024466e6b allow line wrapping for pre style 2021-10-31 21:48:08 +01:00
controlol
289d33c28a add filebrowser menu and fix .map key error 2021-10-31 21:47:34 +01:00
controlol
704648bbc0 add file images 2021-10-31 21:46:06 +01:00
Luc Appelman
959516283f
Do not allow short tags 2021-10-31 10:10:41 +01:00
14 changed files with 4182 additions and 3519 deletions

View File

@ -60,7 +60,7 @@ jobs:
minor_pattern: "(MINOR)"
# Indicate whether short tags like 'v1' should be supported. If false only full
# tags like 'v1.0.0' will be recognized.
short_tags: true
short_tags: false
# A string to determine the format of the version output
format: "${major}.${minor}.${patch}"
# If this is set to true, *every* commit will be treated as a new version.

View File

@ -7,6 +7,7 @@
![GitHub](https://img.shields.io/github/license/controlol/rclone-webui)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/controlol/rclone-webui/Semantic%20Release?label=Build%20Release)
![GitHub milestone](https://img.shields.io/github/milestones/progress/controlol/rclone-webui/3?label=Milestone%20V1.1)
![GitHub top language](https://img.shields.io/github/languages/top/controlol/rclone-webui)
## Description
@ -86,14 +87,16 @@ To build the site you are expected to have npm and nodejs installed and have a a
Get the source files
```
git clone https://github.com/controlol/rclone-webui
git clone ssh://git@code.swissmakers.ch:6022/michael.reber/rclone-webui.git
cd rclone-webui
```
Install dependencies
`dnf install -y nodejs`
`npm install`
`npm ci`
Build the project
`npm run build`
`NODE_OPTIONS=--openssl-legacy-provider npm run build`
The WebUI should have been build in the build folder. Copy the files to a location you can easily access or use the build directory as the source for your rclone rcd.

6776
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,23 @@
import { Component, Fragment } from 'react'
import ReactTooltip from 'react-tooltip'
import { Container, HeaderContainer, InfosContainer, InfosWrapper, ItemsContainer, ActiveContainer, HistoryContainer, HistoryItem, HistoryItemsWrapper, LogoContainer, StatusContainer, StatusBulb, InfosRow } from './styled'
import {
Container,
HeaderContainer,
InfosContainer,
InfosWrapper,
ItemsContainer,
ActiveContainer,
Spinner,
HistoryContainer,
HistoryItem,
HistoryItemsWrapper,
LogoContainer,
StatusContainer,
StatusBulb,
InfosRow,
ActiveChecking
} from './styled'
import API from './utils/API'
import secondsToTimeString from './utils/timestring'
@ -16,87 +32,35 @@ class App extends Component {
super()
this.state = {
stats: {
// bytes: 0,
// checks: 0,
// deletedDirs: 0,
// deletes: 0,
// elapsedTime: 0,
// errors: 0,
// eta: null,
// fatalError: false,
// renames: 0,
// retryError: false,
// speed: 0,
// totalBytes: 0,
// totalChecks: 0,
// totalTransfers: 0,
// transferTime: 0,
// transfers: 0,
// transferring: [
// {
// speed: 23534245,
// group: "job/1",
// size: 23457826345,
// eta: 3600,
// name: "a very very very long absurdly unneccesarily long name this is if you cant tell"
// }
// ]
elapsedTime: 0,
transfers: 0,
bytes: 0,
errors: 0,
lastError: '',
transferring: [],
checking: []
},
remotes: [{
// name: "gdrive",
// type: "drive",
// bytes: 84265292526
}],
remotes: [],
mounts: [],
transferred: [],
version: {
// "arch": "amd64",
// "decomposed": [
// 1,
// 56,
// 2
// ],
// "goTags": "none",
// "goVersion": "go1.16.8",
// "isBeta": false,
// "isGit": false,
// "linking": "static",
// "os": "linux",
// "version": "v1.56.2"
},
transferred: [], // history
version: {},
endPointAvailable: true
}
this.infoInterval = undefined
this.timeInterval = undefined
this.apiInterval = undefined
this.infoInterval = null
this.apiInterval = null
}
componentDidMount = () => {
// fetch initial info
componentDidMount() {
this.fetchRemotes()
this.fetchMounts()
this.fetchVersionInfo()
this.fetchInfos()
// get server stats every 5 seconds
if (this.infoInterval === undefined) this.infoInterval = setInterval(this.fetchInfos, 5000)
// make the view look more responsive by counting every second
if (this.timeInterval === undefined) this.timeInterval = setInterval(() => {
let { stats } = this.state
stats.elapsedTime++
this.setState({ stats })
}, 1000)
// check api status every second
if (this.apiInterval === undefined) this.apiInterval = setInterval(this.checkApiEndpoint, 1000)
this.infoInterval = setInterval(this.fetchInfos, 2000)
this.apiInterval = setInterval(this.checkApiEndpoint, 5000)
}
// clear the intervals
componentWillUnmount = () => {
componentWillUnmount() {
clearInterval(this.infoInterval)
clearInterval(this.timeInterval)
clearInterval(this.apiInterval)
}
@ -106,223 +70,198 @@ class App extends Component {
* also updates the favicon
*/
checkApiEndpoint = () => {
if (API.getEndpointStatus() !== this.state.endPointAvailable) {
this.setState({ endPointAvailable: !this.state.endPointAvailable })
const link = document.querySelectorAll("link[rel~='icon']");
link.forEach(v => {
let segments = v.href.split(".")
if (this.state.endPointAvailable) {
segments[0] = segments[0].substring(0, segments[0].length - 3)
} else {
segments[0] += "-gs"
}
v.href = segments.join(".")
})
const status = API.getEndpointStatus()
if (status !== this.state.endPointAvailable) {
this.setState({ endPointAvailable: status })
if (status) {
this.fetchRemotes()
this.fetchMounts()
this.fetchVersionInfo()
}
}
}
/**
* get the configured remotes
*/
fetchRemotes = () => {
return API.request({
url: "/config/dump",
"_group": "ui"
})
.then(response => {
if (typeof response.data !== "object") throw new Error("invalid response")
let remotes = []
return Promise.all(Object.keys(response.data).map(v => {
return new Promise((resolve, reject) => {
return API.request({
url: "/operations/about",
data: {
fs: v + ":"
}
})
.then(({data}) => {
if (typeof data !== "object" || isNaN(data.used)) return reject()
remotes.push({
name: v,
type: response.data[v].type,
bytes: data.used
})
return resolve()
})
.catch(reject)
})
API.request({ url: '/config/dump', _group: 'ui' })
.then(res => {
if (typeof res.data !== 'object') throw new Error()
const remotes = []
return Promise.all(
Object.keys(res.data).map(name =>
API.request({ url: '/operations/about', data: { fs: name + ':' } })
.then(({ data }) => {
if (typeof data !== 'object' || isNaN(data.used)) throw new Error()
remotes.push({ name, type: res.data[name].type, bytes: data.used })
})
)
).then(() => this.setState({ remotes }))
})
)
.then(() => {
this.setState({ remotes })
})
.catch(err => console.error(err))
})
.catch(() => {})
.catch(() => {})
}
/**
* get the mounted volumes
*/
fetchMounts = () => {
return API.request({
url: "/mount/listmounts"
})
.then(response => {
if (typeof response.data.mountPoints !== "object") throw new Error("invalid response")
this.setState({ mounts: response.data.mountPoints })
})
.catch(() => {})
API.request({ url: '/mount/listmounts' })
.then(res => {
if (!Array.isArray(res.data.mountPoints)) throw new Error()
this.setState({ mounts: res.data.mountPoints })
})
.catch(() => {})
}
/**
* get software versions and architecture info
*/
fetchVersionInfo = () => {
return API.request({
url: "/core/version"
})
.then(response => {
if (typeof response.data !== "object") throw new Error("invalid response")
this.setState({ version: response.data })
})
.catch(() => {})
API.request({ url: '/core/version' })
.then(res => {
if (typeof res.data !== 'object') throw new Error()
this.setState({ version: res.data })
})
.catch(() => {})
}
/**
* gets the server stats
*/
fetchInfos = () => {
return API.request({
url: "/core/stats"
})
.then(response => {
if (typeof response.data !== "object") throw new Error("invalid response")
const stats = response.data
API.request({ url: '/core/stats' })
.then(({ data: stats }) => {
if (typeof stats !== 'object') throw new Error()
if (stats.transfers === 0) return this.setState({ stats })
// store latest stats
this.setState({ stats })
return API.request({
url: "core/transferred"
// update history if transfers > 0
if (stats.transfers > 0) {
API.request({ url: '/core/transferred' })
.then(({ data }) => {
let history = Array.isArray(data.transferred)
? data.transferred
.sort((a, b) => new Date(b.completed_at) - new Date(a.completed_at))
.filter(v => v.error === '' && v.bytes > 0)
: []
history = history.slice(0, 20) // keep last 20
this.setState({ transferred: history })
})
.catch(() => {})
}
})
.then(response => {
if (typeof response.data.transferred !== "object") throw new Error("invalid response")
let transferred = []
response.data.transferred.sort((a,b) => new Date(b.completed_at) - new Date(a.completed_at)).forEach(v => {
if (v.error.length === 0 && v.bytes > 0) return transferred.push(v)
})
this.setState({ transferred, stats })
})
.catch(() => this.setState({ stats }))
})
.catch(() => {})
.catch(() => {})
}
/**
* renders the total speed of all transfers
* @returns {String}
*/
renderLiveSpeed = () => {
const transferring = this.state.stats.transferring
if (typeof transferring !== "object") return "0.00 MB/s";
const list = Array.isArray(this.state.stats.transferring)
? this.state.stats.transferring
: []
let speed = 0
transferring.forEach(v => speed += v.speed)
list.forEach(item => {
speed += typeof item?.speed === 'number' ? item.speed : 0
})
return bytesToString(speed, { speed: true })
}
/**
* renders the list of remotes
* @returns {Component}
*/
renderRemotes = () => {
return this.state.remotes.map(v => (
<InfosRow key={v.name} data-tip={bytesToString(v.bytes, { fixed: 2 })} data-for={"size"+v.MountPoint}>
<p> {v.name} </p>
<p> {v.type} </p>
<ReactTooltip id={"size"+v.MountPoint} place="left" type="info" effect="solid" />
const { remotes } = this.state
if (!remotes.length) return null
return remotes.map(r => (
<InfosRow
key={r.name}
data-tip={bytesToString(r.bytes, { fixed: 2 })}
data-for={'size-' + r.name}
>
<p>{r.name}</p>
<p>{r.type}</p>
<ReactTooltip
id={'size-' + r.name}
place="left"
type="info"
effect="solid"
globalEventOff="click"
/>
</InfosRow>
))
}
/**
* renders the list of mounts
* @returns {Component}
*/
renderMounts = () => {
return this.state.mounts.map(v => (
<Fragment key={v.MountPoint} >
<p> {v.Fs} </p>
<p> {v.MountPoint} </p>
const { mounts } = this.state
if (!mounts.length) return null
return mounts.map(m => (
<Fragment key={m.MountPoint}>
<p>{m.Fs}</p>
<p>{m.MountPoint}</p>
</Fragment>
))
}
/**
* Each job gets it's own component and is rendered if there are any
* Jobs are found by their transfer group
* @returns {Job}
*/
renderActiveJobs = () => {
if (typeof this.state.stats.transferring !== "object") return;
const { transferring = [], checking = [] } = this.state.stats
// collapse if nothing
if (!transferring.length && !checking.length) return null
let activeJobIds = []
// group transfers by job
const groups = transferring.reduce((acc, item) => {
if (!item.group) return acc
acc[item.group] = acc[item.group] || []
acc[item.group].push(item)
return acc
}, {})
const { transferring } = this.state.stats
// map to <Job>
const transferJobs = Object.entries(groups).map(([grp, arr]) => (
<Job
key={grp}
jobid={grp.replace(/\D/g, '')}
refreshStats={this.fetchInfos}
initialStats={{ transferring: arr }}
/>
))
transferring.forEach(v => {
if (!activeJobIds.includes(v.group)) activeJobIds.push(v.group)
})
// now render checking rows
const checkingRows = checking.map((name, i) => (
<ActiveChecking key={i}>
<p>{name}</p>
<p></p>
<p>Checking</p>
<p></p>
</ActiveChecking>
))
return activeJobIds.map(group => {
const fileTransfers = transferring.filter(v => v.group === group)
// if no transfers but checks present, just show checks
if (!transferJobs.length && checkingRows.length) {
return <Fragment>{checkingRows}</Fragment>
}
return <Job key={group} fileTransfers={fileTransfers} jobid={group.replace(/\D/g, '')} refreshStats={this.fetchInfos} />
})
// otherwise show transfers then checks
return (
<Fragment>
{transferJobs}
{checkingRows}
</Fragment>
)
}
/**
* Renders all history items
* @returns {Component[]}
*/
renderHistory = () => {
return this.state.transferred.map((v, i) => (
<HistoryItem key={"transfer" + i}>
<p> { v.name } </p>
<p> { new Date(v.completed_at).toLocaleString() } </p>
renderHistory = () =>
this.state.transferred.map((v, i) => (
<HistoryItem key={i}>
<p>{v.name}</p>
<p>{new Date(v.completed_at).toLocaleString()}</p>
</HistoryItem>
))
}
render = () => {
const { stats, version, endPointAvailable } = this.state
const { elapsedTime, transfers, totalTransfers, bytes, errors, lastError } = stats
render() {
const { stats, version, endPointAvailable, remotes, mounts } = this.state
const { elapsedTime, transfers, bytes, errors, lastError, transferring } = stats
return (
<Fragment>
<HeaderContainer>
<LogoContainer>
<img src="/favicon-64x64.png" alt="Rclone WebUI logo" width="64" height="64" />
<h1> Rclone Dashboard </h1>
<img src="/favicon-64x64.png" alt="Rclone Dashboard" width="64" height="64" />
<h1>Rclone Dashboard</h1>
</LogoContainer>
<StatusContainer>
<StatusBulb style={{ background: endPointAvailable ? "var(--status-green)" : "var(--status-red)" }} />
{ endPointAvailable ? "API endpoint is behaving normally" : "API endpoint is unavailable" }
<StatusBulb
style={{
background: endPointAvailable ? 'var(--status-green)' : 'var(--status-red)'
}}
/>
{endPointAvailable
? 'API endpoint is behaving normally'
: 'API endpoint is unavailable'}
</StatusContainer>
</HeaderContainer>
@ -330,60 +269,63 @@ class App extends Component {
<Container>
<ItemsContainer>
<ActiveContainer>
<h1> Active Jobs </h1>
{ this.renderActiveJobs() }
</ActiveContainer>
<ActiveContainer>
<h1>
Active Jobs
{/* show spinner if any checking or transferring */}
{(
(this.state.stats.transferring?.length || 0) +
(this.state.stats.checking?.length || 0)
) > 0 && <Spinner />}
</h1>
{this.renderActiveJobs()}
</ActiveContainer>
<HistoryContainer>
<h1> History </h1>
<HistoryItemsWrapper>
{ this.renderHistory() }
</HistoryItemsWrapper>
<h1>History</h1>
<HistoryItemsWrapper>{this.renderHistory()}</HistoryItemsWrapper>
</HistoryContainer>
</ItemsContainer>
<InfosContainer>
<InfosWrapper style={{ minHeight: "10rem" }}>
<h2> Stats </h2>
<p> Uptime </p>
<p> { secondsToTimeString(elapsedTime, true) } </p>
<p> Speed </p>
<p> { this.renderLiveSpeed() } </p>
<p> Active transfers </p>
<p> { transfers + (transfers === 1 ? " file" : " files" )} </p>
<p> Total transfered files </p>
<p> { totalTransfers + (totalTransfers === 1 ? " file" : " files" )} </p>
<p> Total transferred data </p>
<p> { bytesToString(bytes, {}) } </p>
<InfosWrapper style={{ minHeight: '10rem' }}>
<h2>Service Stats</h2>
<p>Uptime</p>
<p>{secondsToTimeString(elapsedTime, true)}</p>
<p>Speed</p>
<p>{this.renderLiveSpeed()}</p>
<p>Active transfers</p>
<p>{`${(transferring || []).length} files`}</p>
<p>Total transferred files</p>
<p>{`${transfers || 0} files`}</p>
<p>Total transferred data</p>
<p>{bytesToString(bytes, {})}</p>
</InfosWrapper>
<InfosWrapper style={{ minHeight: "6rem" }}>
<h2> Remotes </h2>
{ this.renderRemotes() }
</InfosWrapper>
<InfosWrapper style={{ minHeight: "4.5rem" }}>
<h2> Mounts </h2>
{ this.renderMounts() }
</InfosWrapper>
<Settings />
<InfosWrapper>
<h2> System Info </h2>
<p> Rclone version </p>
<p> { version.version } </p>
<p> GO version </p>
<p> { version.goVersion } </p>
<p> Architecture </p>
<p> { version.arch } </p>
<h2>Environment</h2>
<p>Rclone version</p>
<p>{version.version}</p>
<p>GO version</p>
<p>{version.goVersion}</p>
<p>Architecture</p>
<p>{version.arch}</p>
</InfosWrapper>
{remotes.length > 0 && (
<InfosWrapper style={{ minHeight: '6rem' }}>
<h2>Remotes</h2>
{this.renderRemotes()}
</InfosWrapper>
)}
{mounts.length > 0 && (
<InfosWrapper style={{ minHeight: '4.5rem' }}>
<h2>Mounts</h2>
{this.renderMounts()}
</InfosWrapper>
)}
<Settings />
</InfosContainer>
</Container>
</Fragment>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<path fill="#3f79ad" d="M13 0.9l-1 1.1h-12v14h14v-10.5l1.7-2-2.7-2.6zM6.5 11.7l-4.2-4.2 1.4-1.4 2.7 2.7 6.6-6.6 1.4 1.4-7.9 8.1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<path fill="#3f79ad" d="M14 6.2v7.8h-12v-12h10.5l1-1h-12.5v14h14v-9.8z"></path>
<path fill="#3f79ad" d="M7.9 10.9l-4.2-4.2 1.5-1.4 2.7 2.8 6.7-6.7 1.4 1.4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -0,0 +1,52 @@
import styled, { keyframes } from 'styled-components'
const BarWidthAnimation = keyframes`
0% {
width: 70%;
}
100% {
width: 30%;
}
`
const BarPositionAnimation = keyframes`
0% {
left: -35%;
}
100% {
left: 135%;
}
`
const LoaderContainer = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
background-color: rgba(255,255,255,.1);
overflow: hidden;
::before, ::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 2px;
width: 100%;
background-color: var(--primary-color);
}
::after {
background-color: var(--secondary-color);
transform: translateX(-50%);
animation: ${BarWidthAnimation} 2s infinite alternate, ${BarPositionAnimation} 2s infinite;
}
`
const LineLoader = props => {
return <LoaderContainer />
}
export default LineLoader

View File

@ -15,11 +15,11 @@ export const ErrorContainer = styled.div`
`
const Error = ({ errorCount, lastError }) => {
const errorCountString = errorCount + ( errorCount === 1 ? " Error" : " Errors" )
const errorCountString = (errorCount ? errorCount : 0) + ( errorCount === 1 ? " Error" : " Errors" )
const imageSource = errorCount > 0 ? ErrorCircle : CheckCircle
const ErrorToolTipString = errorCount > 0 ? `${errorCountString()} since Rclone started<br/>Last error: ${lastError}` : "0 errors since Rclone started"
const ErrorToolTipString = errorCount > 0 ? `${errorCountString} since Rclone started<br/>Last error: ${lastError}` : "0 errors since Rclone started"
return (
<ErrorContainer data-tip={ErrorToolTipString} data-for="error-cnt" >

View File

@ -1,129 +1,114 @@
import { Component, Fragment } from "react"
import { ActiveJob, ActiveTransfer, StopButton } from "../styled"
import { Component, Fragment } from 'react'
import { ActiveJob, ActiveTransfer, StopButton } from '../styled'
import API from '../utils/API'
import bytesToString from "../utils/bytestring"
import secondsToTimeString from "../utils/timestring"
import bytesToString from '../utils/bytestring'
import secondsToTimeString from '../utils/timestring'
class Job extends Component {
constructor() {
super()
constructor(props) {
super(props)
this.state = {
jobid: props.jobid,
stats: {
elapsedTime: 0,
eta: 3600,
bytes: 1024 * 1024 * 128,
totalBytes: 1024 * 1024 * 1024,
transferring: [
{
speed: 23534245,
group: "job/1",
size: 23457826345,
eta: 3600,
name: "a very very very long absurdly unneccesarily long name this is if you cant tell"
}
]
},
jobid: 0
eta: 0,
bytes: 0,
totalBytes: 0,
transferring: props.initialStats?.transferring ?? []
}
}
this.decreaseEtaInterval = undefined
this.fetchStatsInterval = undefined
this.fetchStatsInterval = null
this.decreaseEtaInterval = null
}
componentDidMount = () => {
this.fetchStats()
// make the view look more responsive by counting every second
if (this.decreaseEtaInterval === undefined) this.decreaseEtaInterval = setInterval(() => {
let { stats } = this.state
if (stats.eta > 1) stats.eta--
stats.elapsedTime++
stats.transferring?.forEach(v => {if (v.eta > 1) v.eta--})
this.setState({ stats })
componentDidMount() {
if (this.state.jobid) {
this.fetchStats()
this.fetchStatsInterval = setInterval(this.fetchStats, 5000)
}
this.decreaseEtaInterval = setInterval(() => {
this.setState(({ stats }) => ({
stats: {
...stats,
elapsedTime: stats.elapsedTime + 1,
eta: Math.max(0, stats.eta - 1),
transferring: (stats.transferring || []).map(v => ({
...v,
eta: Math.max(0, v.eta - 1)
}))
}
}))
}, 1000)
// get job stats every 5 seconds
if (this.fetchStatsInterval === undefined) this.fetchStatsInterval = setInterval(this.fetchStats, 5 * 1000)
}
// clear the intervals
componentWillUnmount = () => {
clearInterval(this.decreaseEtaInterval)
componentWillUnmount() {
clearInterval(this.fetchStatsInterval)
clearInterval(this.decreaseEtaInterval)
}
/**
* get the stats from the job this component displays
*/
fetchStats = () => {
return API.request({
url: "/core/stats",
data: {
group: "job/" + this.props.jobid
}
})
.then(response => {
if (typeof response.data !== "object") return new Error("invalid response")
fetchStats = () =>
API.request({ url: '/core/stats', data: { group: `job/${this.state.jobid}` } })
.then(({ data }) => {
if (typeof data === 'object') {
this.setState({
stats: {
elapsedTime: data.elapsedTime ?? 0,
eta: data.eta ?? 0,
bytes: data.bytes ?? 0,
totalBytes: data.totalBytes ?? 0,
transferring: Array.isArray(data.transferring) ? data.transferring : []
}
})
}
})
.catch(() => {})
this.setState({ stats: response.data })
})
.catch(() => {})
}
/**
* Immediately cancels the current job
*/
stopJob = () => {
return API.request({
url: "/job/stop",
data: {
jobid: this.props.jobid
}
})
.then(() => this.props.refreshStats())
.catch(() => {})
}
/**
* render each transfer with info
*/
renderActiveTransfer = () => {
const { transferring } = this.state.stats
if (typeof transferring !== "object") return;
stopJob = () =>
API.request({ url: '/job/stop', data: { jobid: this.state.jobid } })
.then(() => this.props.refreshStats())
.catch(() => {})
renderActiveTransfer() {
const { transferring = [] } = this.state.stats
if (!transferring.length) return null
return transferring.map(v => (
<ActiveTransfer key={v.name}>
<p> { v.name } </p>
<p> { bytesToString(v.size, {}) } </p>
<p> { secondsToTimeString(v.eta) } </p>
<p> { bytesToString(v.speed, { speed: true }) } </p>
<p>{v.name}</p>
<p>{bytesToString(v.size, {})}</p>
<p>{secondsToTimeString(v.eta)}</p>
<p>{bytesToString(v.speed, { speed: true })}</p>
</ActiveTransfer>
))
}
render = () => {
const { stats } = this.state
render() {
const { stats, jobid } = this.state
return (
<Fragment>
<ActiveJob>
<p>Time elapsed:</p>
<p>{secondsToTimeString(stats.elapsedTime)}</p>
<ActiveJob>
<p> Time elapsed: </p>
<p> { secondsToTimeString(stats.elapsedTime) } </p>
<p> Progress: </p>
<p> { bytesToString(stats.bytes, { format: "GB", fixed: 3 }) } / { bytesToString(stats.totalBytes, { format: "GB", fixed: 3 }) } GB </p>
<p>Progress:</p>
<p>
{bytesToString(stats.bytes, { format: 'GB', fixed: 3 })} /{' '}
{bytesToString(stats.totalBytes, { format: 'GB', fixed: 3 })} GB
</p>
<p> Time left: </p>
<p> { secondsToTimeString(stats.eta) } </p>
<p> Progress: </p>
<p> { ((stats.bytes / stats.totalBytes) * 100 || 0).toFixed(2) } % </p>
<p>Time left:</p>
<p>{secondsToTimeString(stats.eta)}</p>
<StopButton onClick={this.stopJob}> Cancel </StopButton>
</ActiveJob>
{ this.renderActiveTransfer() }
<p>Progress:</p>
<p>{((stats.bytes / stats.totalBytes) * 100).toFixed(2)} %</p>
<p>Speed:</p>
<p>{bytesToString(stats.speed, { speed: true })}</p>
{jobid ? <StopButton onClick={this.stopJob}>Cancel</StopButton> : null}
</ActiveJob>
{/* Transfers only */}
{this.renderActiveTransfer()}
</Fragment>
)
}

View File

@ -1,13 +1,22 @@
import styled from 'styled-components'
import { Container } from '../styled'
import { Button as normalButton, Container } from '../styled'
import Error from './error'
const NavigationContainer = styled(Container)`
gap: 0 .5rem;
@media only screen and (max-width: 800px) {
justify-content: center;
}
`
const Button = styled(normalButton)`
display: flex;
align-items: center;
gap: 0 .3rem;
`
const Navigation = ({ info }) => {
const { errors, lastError } = info

View File

@ -24,11 +24,13 @@ class Settings extends Component {
"--popup-background": "#222",
"--popup-header": "#333",
"--box-gradient": "linear-gradient(#71caf220, #3f79ad33)",
"--box-radial-gradient": "radial-gradient(#71caf220, transparent 150%)",
"--status-red": "linear-gradient(#f56565aa, #c92222)",
"--status-green": "linear-gradient(#95ee85aa, #3c891c)",
"--text-color": "#fff",
"--text-hover": "#eee",
"--button-color": "#111",
"--button-hover": "#181818"
"--button-hover": "#1a1a1a"
}
}
@ -125,7 +127,7 @@ class Settings extends Component {
renderSettings = () => {
return (
<PopupContainer>
<PopupTitle> Settings </PopupTitle>
<PopupTitle> Rclone Settings </PopupTitle>
<Cross onClick={() => this.setState({ show: false })}> Close </Cross>
<pre>
@ -149,7 +151,7 @@ class Settings extends Component {
}
<InfosWrapper>
<h2> Settings </h2>
<h2> Rclone Settings </h2>
<p> Theme </p>
<Switch
checked={darkTheme}
@ -177,7 +179,7 @@ class Settings extends Component {
<p> File min age </p>
<p> { secondsToTimeString(settings?.filter?.MinAge / 1000000000, true) } </p>
<Button onClick={this.showSettings}> List Settings </Button>
<Button onClick={this.showSettings}> Show full Configuration </Button>
</InfosWrapper>
</Fragment>
)

View File

@ -1,19 +1,27 @@
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap');
html {
--scrollbarBG: #CFD8DC;
--off-white: #adb2b5;
--thumbBG: #90A4AE;
--background-color: #eee;
--popup-background: #ddd;
--popup-header: #ccc;
--primary-color: #3f79ad;
--secondary-color: #71caf2;
--tertiary-color: #b4e3f9;
--primary-color-trans: #3f79ad33;
--secondary-color-trans: #71caf233;
--text-color: #000;
--text-hover: #222;
--box-gradient: linear-gradient(#71caf220, #3f79ad33);
--box-radial-gradient: radial-gradient(#3f79ad33, transparent 150%);
--status-red: linear-gradient(#fd4444aa, #c92222);
--status-green: linear-gradient(#3bc322aa, #3c891c);
--warning-red: var(--status-red);
--button-color: #fafafa;
--button-hover: #e4e4e4;
@ -25,6 +33,22 @@ html {
color: var(--text-color);
}
@media (prefers-color-scheme: dark) {
html {
--background-color: #000;
--popup-background: #222;
--popup-header: #333;
--box-gradient: linear-gradient(#71caf220, #3f79ad33);
--box-radial-gradient: radial-gradient(#71caf220, transparent 150%);
--status-red: linear-gradient(#f56565aa, #c92222);
--status-green: linear-gradient(#95ee85aa, #3c891c);
--text-color: #fff;
--text-hover: #eee;
--button-color: #111;
--button-hover: #1a1a1a;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@ -39,4 +63,27 @@ body {
padding: 0;
margin: 0;
box-sizing: border-box;
}
}
pre {
white-space: pre-wrap;
}
*::-webkit-scrollbar {
width: 11px;
}
body {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG) ;
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}

View File

@ -1,5 +1,7 @@
import styled from 'styled-components'
import Checked from './assets/icons/checked.svg'
export const Container = styled.div`
display: flex;
gap: 0 2rem;
@ -110,6 +112,7 @@ export const ActiveJob = styled.div`
export const ActiveTransfer = styled.div`
border-radius: .3rem;
border: 1px solid var(--secondary-color);
font-family: monospace;
background-color: var(--secondary-color-trans);
padding: .5rem;
margin-left: 1.5rem;
@ -138,6 +141,28 @@ export const ActiveTransfer = styled.div`
}
`
/* Same grid but a softer colour so checks are distinguishable */
export const ActiveChecking = styled(ActiveTransfer)`
background: var(--tertiary-color-trans);
`;
export const Spinner = styled.div`
display: inline-block;
width: 1.2em; /* size relative to font */
height: 1.2em;
margin-left: 0.5em; /* space after the text */
vertical-align: middle; /* align with text baseline */
border: 0.2em solid var(--secondary-color-trans);
border-top: 0.2em solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`
export const HistoryContainer = styled(ActiveContainer)`
margin-top: 1rem;
`
@ -281,6 +306,9 @@ export const PopupContainer = styled.div`
padding: 1rem 2rem;
overflow-y: auto;
z-index: 999;
display: flex;
flex-direction: column;
gap: 1rem 0;
::-webkit-scrollbar {
display: none;
@ -288,11 +316,6 @@ export const PopupContainer = styled.div`
scrollbar-width: none; /* Firefox */
}
pre {
margin-top: 4.5rem;
white-space: pre-wrap;
}
@media only screen and (max-width: 800px) {
width: 100vw;
height: 100vh;
@ -307,15 +330,13 @@ export const PopupTitle = styled.p`
text-align: center;
font-weight: 600;
background-color: var(--popup-header);
position: fixed;
transform: translateX(-50%);
left: 50%;
width: 80vw;
padding: 1rem 0;
margin-top: -1rem;
margin: -1rem 0 0 -2rem;
@media only screen and (max-width: 800px) {
width: 100vw;
margin-left: -.5rem;
}
`
@ -324,10 +345,12 @@ export const Cross = styled.div`
position: fixed;
z-index: 20;
right: 11.5vw;
top: calc(3vh + .5rem);
font-size: 1.25rem;
@media only screen and (max-width: 800px) {
right: 5vw;
top: .5rem;
}
`
@ -336,4 +359,54 @@ export const IconWrapper = styled.div`
justify-content: center;
align-items: center;
height: 100%;
`
export const Checkbox = styled.input`
display: none;
&[type=checkbox] + label {
position: relative;
margin-left: 1.5rem;
}
&[type=checkbox] + label::after {
content: '';
position: absolute;
top: .35rem;
left: -1.45rem;
width: .75rem;
height: .75rem;
outline: .1px solid var(--primary-color);
}
&[type=checkbox]:checked + label::after {
background: url(${Checked});
top: .15rem;
left: -1.5rem;
width: 1rem;
height: 1rem;
outline: unset;
}
`
export const Input = styled.input`
width: 100%;
padding: .6em 1em;
margin: .5em 0;
display: inline-block;
border-radius: 4px;
border: 1px solid var(--primary-color);
background-color: var(--button-color);
color: white;
transition: border .3s ease-in-out;
font-size: .9rem;
&:focus {
outline: none;
background-color: var(--button-hover);
}
`
export const WarningButton = styled(Button)`
background: var(--warning-red);
`

View File

@ -37,7 +37,10 @@ class API {
}
// log error and reject
console.error(err)
if (err?.response?.data?.error) {
console.error(err.response.status, err.response.data.error)
return reject(err.response.data.error)
}
return reject()
})
})