Rework project and also include checking monitoring
This commit is contained in:
parent
d51b815b92
commit
9fbbf821f0
514
src/App.js
514
src/App.js
@ -1,7 +1,23 @@
|
|||||||
import { Component, Fragment } from 'react'
|
import { Component, Fragment } from 'react'
|
||||||
import ReactTooltip from 'react-tooltip'
|
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 API from './utils/API'
|
||||||
import secondsToTimeString from './utils/timestring'
|
import secondsToTimeString from './utils/timestring'
|
||||||
@ -10,105 +26,41 @@ import bytesToString from './utils/bytestring'
|
|||||||
import Settings from './components/settings'
|
import Settings from './components/settings'
|
||||||
import Job from './components/job'
|
import Job from './components/job'
|
||||||
import Navigation from './components/navigation'
|
import Navigation from './components/navigation'
|
||||||
//import FileBrowserMenu from './components/fileBrowserMenu'
|
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.state = {
|
this.state = {
|
||||||
stats: {
|
stats: {
|
||||||
// bytes: 0,
|
elapsedTime: 0,
|
||||||
//checking: [
|
transfers: 0,
|
||||||
// "a very very very long absurdly unneccesarily long name this is if you cant tell",
|
bytes: 0,
|
||||||
// "a very very very long absurdly unneccesarily long name this is if you cant tell 2"
|
errors: 0,
|
||||||
//]
|
lastError: '',
|
||||||
// checks: 0,
|
transferring: [],
|
||||||
// deletedDirs: 0,
|
checking: []
|
||||||
// deletes: 0,
|
|
||||||
// elapsedTime: 0,
|
|
||||||
// errors: 0,
|
|
||||||
// eta: null,
|
|
||||||
// fatalError: false,
|
|
||||||
// renames: 0,
|
|
||||||
// retryError: false,
|
|
||||||
// speed: 0,
|
|
||||||
// totalBytes: 0,
|
|
||||||
// totalChecks: 0,
|
|
||||||
// totalTransfers: 0,
|
|
||||||
// transferTime: 0,
|
|
||||||
// transferring: [
|
|
||||||
// {
|
|
||||||
// bytes: 590673
|
|
||||||
// dstFs: "/mnt/backup01/data_replica"
|
|
||||||
// eta: 0
|
|
||||||
// group: "job/17553"
|
|
||||||
// name: "a very very very long absurdly unneccesarily long name this is if you cant tell"
|
|
||||||
// percentage: 100
|
|
||||||
// size: 590673
|
|
||||||
// speed: 806565.7017727684
|
|
||||||
// speedAvg: 574257.5080241694
|
|
||||||
// srcFs: "/mnt/main_storage/data"
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
// transfers: 0,
|
|
||||||
},
|
},
|
||||||
//remotes: [{
|
|
||||||
// name: "gdrive",
|
|
||||||
// type: "drive",
|
|
||||||
// bytes: 84265292526
|
|
||||||
//}],
|
|
||||||
remotes: [],
|
remotes: [],
|
||||||
mounts: [],
|
mounts: [],
|
||||||
transferred: [],
|
transferred: [], // history
|
||||||
version: {
|
version: {},
|
||||||
// "arch": "amd64",
|
endPointAvailable: true
|
||||||
// "decomposed": [
|
|
||||||
// 1,
|
|
||||||
// 56,
|
|
||||||
// 2
|
|
||||||
// ],
|
|
||||||
// "goTags": "none",
|
|
||||||
// "goVersion": "go1.16.8",
|
|
||||||
// "isBeta": false,
|
|
||||||
// "isGit": false,
|
|
||||||
// "linking": "static",
|
|
||||||
// "os": "linux",
|
|
||||||
// "version": "v1.56.2"
|
|
||||||
},
|
|
||||||
endPointAvailable: true,
|
|
||||||
renderBrowser: false
|
|
||||||
}
|
}
|
||||||
|
this.infoInterval = null
|
||||||
this.infoInterval = undefined
|
this.apiInterval = null
|
||||||
this.timeInterval = undefined
|
|
||||||
this.apiInterval = undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount() {
|
||||||
// fetch initial info
|
|
||||||
this.fetchRemotes()
|
this.fetchRemotes()
|
||||||
this.fetchMounts()
|
this.fetchMounts()
|
||||||
this.fetchVersionInfo()
|
this.fetchVersionInfo()
|
||||||
this.fetchInfos()
|
this.fetchInfos()
|
||||||
|
this.infoInterval = setInterval(this.fetchInfos, 2000)
|
||||||
// get server stats every 1 seconds
|
this.apiInterval = setInterval(this.checkApiEndpoint, 5000)
|
||||||
if (this.infoInterval === undefined) this.infoInterval = setInterval(this.fetchInfos, 1000)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear the intervals
|
componentWillUnmount() {
|
||||||
componentWillUnmount = () => {
|
|
||||||
clearInterval(this.infoInterval)
|
clearInterval(this.infoInterval)
|
||||||
clearInterval(this.timeInterval)
|
|
||||||
clearInterval(this.apiInterval)
|
clearInterval(this.apiInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,21 +72,7 @@ class App extends Component {
|
|||||||
checkApiEndpoint = () => {
|
checkApiEndpoint = () => {
|
||||||
const status = API.getEndpointStatus()
|
const status = API.getEndpointStatus()
|
||||||
if (status !== this.state.endPointAvailable) {
|
if (status !== this.state.endPointAvailable) {
|
||||||
this.setState({ endPointAvailable: !this.state.endPointAvailable })
|
this.setState({ endPointAvailable: status })
|
||||||
|
|
||||||
const link = document.querySelectorAll("link[rel~='icon']");
|
|
||||||
|
|
||||||
link.forEach(v => {
|
|
||||||
let segments = v.href.split(".")
|
|
||||||
if (status) {
|
|
||||||
segments[0] = segments[0].substring(0, segments[0].length - 3)
|
|
||||||
} else {
|
|
||||||
segments[0] += "-gs"
|
|
||||||
}
|
|
||||||
v.href = segments.join(".")
|
|
||||||
})
|
|
||||||
|
|
||||||
// get info after api recovers
|
|
||||||
if (status) {
|
if (status) {
|
||||||
this.fetchRemotes()
|
this.fetchRemotes()
|
||||||
this.fetchMounts()
|
this.fetchMounts()
|
||||||
@ -143,239 +81,187 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get the configured remotes
|
|
||||||
*/
|
|
||||||
fetchRemotes = () => {
|
fetchRemotes = () => {
|
||||||
return API.request({
|
API.request({ url: '/config/dump', _group: 'ui' })
|
||||||
url: "/config/dump",
|
.then(res => {
|
||||||
"_group": "ui"
|
if (typeof res.data !== 'object') throw new Error()
|
||||||
})
|
const remotes = []
|
||||||
.then(response => {
|
return Promise.all(
|
||||||
if (typeof response.data !== "object") throw new Error("invalid response")
|
Object.keys(res.data).map(name =>
|
||||||
|
API.request({ url: '/operations/about', data: { fs: name + ':' } })
|
||||||
let remotes = []
|
.then(({ data }) => {
|
||||||
|
if (typeof data !== 'object' || isNaN(data.used)) throw new Error()
|
||||||
return Promise.all(Object.keys(response.data).map(v => {
|
remotes.push({ name, type: res.data[name].type, bytes: data.used })
|
||||||
return new Promise((resolve, reject) => {
|
})
|
||||||
return API.request({
|
)
|
||||||
url: "/operations/about",
|
).then(() => this.setState({ remotes }))
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
)
|
.catch(() => {})
|
||||||
.then(() => {
|
|
||||||
this.setState({ remotes })
|
|
||||||
})
|
|
||||||
.catch(err => console.error(err))
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get the mounted volumes
|
|
||||||
*/
|
|
||||||
fetchMounts = () => {
|
fetchMounts = () => {
|
||||||
return API.request({
|
API.request({ url: '/mount/listmounts' })
|
||||||
url: "/mount/listmounts"
|
.then(res => {
|
||||||
})
|
if (!Array.isArray(res.data.mountPoints)) throw new Error()
|
||||||
.then(response => {
|
this.setState({ mounts: res.data.mountPoints })
|
||||||
if (typeof response.data.mountPoints !== "object") throw new Error("invalid response")
|
})
|
||||||
|
.catch(() => {})
|
||||||
this.setState({ mounts: response.data.mountPoints })
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get software versions and architecture info
|
|
||||||
*/
|
|
||||||
fetchVersionInfo = () => {
|
fetchVersionInfo = () => {
|
||||||
return API.request({
|
API.request({ url: '/core/version' })
|
||||||
url: "/core/version"
|
.then(res => {
|
||||||
})
|
if (typeof res.data !== 'object') throw new Error()
|
||||||
.then(response => {
|
this.setState({ version: res.data })
|
||||||
if (typeof response.data !== "object") throw new Error("invalid response")
|
})
|
||||||
|
.catch(() => {})
|
||||||
this.setState({ version: response.data })
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* gets the server stats
|
|
||||||
*/
|
|
||||||
fetchInfos = () => {
|
fetchInfos = () => {
|
||||||
return API.request({
|
API.request({ url: '/core/stats' })
|
||||||
url: "/core/stats"
|
.then(({ data: stats }) => {
|
||||||
})
|
if (typeof stats !== 'object') throw new Error()
|
||||||
.then(response => {
|
|
||||||
if (typeof response.data !== "object") throw new Error("invalid response")
|
|
||||||
const stats = response.data
|
|
||||||
|
|
||||||
if (stats.transfers === 0) return this.setState({ stats })
|
// store latest stats
|
||||||
|
this.setState({ stats })
|
||||||
|
|
||||||
return API.request({
|
// update history if transfers > 0
|
||||||
url: "core/transferred"
|
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 => {
|
.catch(() => {})
|
||||||
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(() => {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* renders the total speed of all transfers
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
renderLiveSpeed = () => {
|
renderLiveSpeed = () => {
|
||||||
const transferring = this.state.stats.transferring
|
const list = Array.isArray(this.state.stats.transferring)
|
||||||
|
? this.state.stats.transferring
|
||||||
if (typeof transferring !== "object") return "0.00 MB/s";
|
: []
|
||||||
|
|
||||||
let speed = 0
|
let speed = 0
|
||||||
|
list.forEach(item => {
|
||||||
transferring.forEach(v => speed += v.speed)
|
speed += typeof item?.speed === 'number' ? item.speed : 0
|
||||||
|
})
|
||||||
return bytesToString(speed, { speed: true })
|
return bytesToString(speed, { speed: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* renders the list of remotes
|
|
||||||
* @returns {Component}
|
|
||||||
*/
|
|
||||||
renderRemotes = () => {
|
renderRemotes = () => {
|
||||||
const { remotes } = this.state
|
const { remotes } = this.state
|
||||||
if (remotes.length === 0) return null
|
if (!remotes.length) return null
|
||||||
|
return remotes.map(r => (
|
||||||
return remotes.map(v => (
|
|
||||||
<InfosRow
|
<InfosRow
|
||||||
key={"mount" + v.name}
|
key={r.name}
|
||||||
data-tip={bytesToString(v.bytes, { fixed: 2 })}
|
data-tip={bytesToString(r.bytes, { fixed: 2 })}
|
||||||
data-for={"size"+v.MountPoint}>
|
data-for={'size-' + r.name}
|
||||||
|
>
|
||||||
<p>{v.name}</p>
|
<p>{r.name}</p>
|
||||||
<p>{v.type}</p>
|
<p>{r.type}</p>
|
||||||
{/* add EDIT button */}
|
<ReactTooltip
|
||||||
<ReactTooltip id={"size"+v.MountPoint} place="left" type="info" effect="solid" globalEventOff="click" />
|
id={'size-' + r.name}
|
||||||
|
place="left"
|
||||||
|
type="info"
|
||||||
|
effect="solid"
|
||||||
|
globalEventOff="click"
|
||||||
|
/>
|
||||||
</InfosRow>
|
</InfosRow>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* renders the list of mounts
|
|
||||||
* @returns {Component}
|
|
||||||
*/
|
|
||||||
renderMounts = () => {
|
renderMounts = () => {
|
||||||
const { mounts } = this.state
|
const { mounts } = this.state
|
||||||
if (mounts.length === 0) return null
|
if (!mounts.length) return null
|
||||||
|
return mounts.map(m => (
|
||||||
return mounts.map(v => (
|
<Fragment key={m.MountPoint}>
|
||||||
<Fragment key={v.MountPoint}>
|
<p>{m.Fs}</p>
|
||||||
<p>{v.Fs}</p>
|
<p>{m.MountPoint}</p>
|
||||||
<p>{v.MountPoint}</p>
|
|
||||||
</Fragment>
|
</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 = () => {
|
renderActiveJobs = () => {
|
||||||
const transferring = this.state.stats.transferring
|
const { transferring = [], checking = [] } = this.state.stats
|
||||||
|
// collapse if nothing
|
||||||
|
if (!transferring.length && !checking.length) return null
|
||||||
|
|
||||||
// If it's not an array or empty, render nothing
|
// group transfers by job
|
||||||
if (!Array.isArray(transferring) || transferring.length === 0) {
|
const groups = transferring.reduce((acc, item) => {
|
||||||
return null
|
if (!item.group) return acc
|
||||||
|
acc[item.group] = acc[item.group] || []
|
||||||
|
acc[item.group].push(item)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
// 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 }}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
// now render checking rows
|
||||||
|
const checkingRows = checking.map((name, i) => (
|
||||||
|
<ActiveChecking key={i}>
|
||||||
|
<p>{name}</p>
|
||||||
|
<p>—</p>
|
||||||
|
<p>Checking</p>
|
||||||
|
<p>—</p>
|
||||||
|
</ActiveChecking>
|
||||||
|
))
|
||||||
|
|
||||||
|
// if no transfers but checks present, just show checks
|
||||||
|
if (!transferJobs.length && checkingRows.length) {
|
||||||
|
return <Fragment>{checkingRows}</Fragment>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect unique, defined group IDs
|
// otherwise show transfers then checks
|
||||||
const activeJobIds = Array.from(
|
return (
|
||||||
new Set(
|
<Fragment>
|
||||||
transferring
|
{transferJobs}
|
||||||
.map(v => v.group)
|
{checkingRows}
|
||||||
.filter(g => typeof g === 'string')
|
</Fragment>
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return activeJobIds.map(group => {
|
|
||||||
// Protection: convert to string
|
|
||||||
const safeGroup = String(group)
|
|
||||||
const jobid = safeGroup.replace(/\D/g, '')
|
|
||||||
|
|
||||||
// All transfers in this group
|
|
||||||
const fileTransfers = transferring.filter(v => v.group === group)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Job
|
|
||||||
key={safeGroup}
|
|
||||||
fileTransfers={fileTransfers}
|
|
||||||
jobid={jobid}
|
|
||||||
refreshStats={this.fetchInfos}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
renderHistory = () =>
|
||||||
* Renders all history items
|
this.state.transferred.map((v, i) => (
|
||||||
* @returns {Component[]}
|
<HistoryItem key={i}>
|
||||||
*/
|
<p>{v.name}</p>
|
||||||
renderHistory = () => {
|
<p>{new Date(v.completed_at).toLocaleString()}</p>
|
||||||
return this.state.transferred.map((v, i) => (
|
|
||||||
<HistoryItem key={"transfer" + i}>
|
|
||||||
<p> { v.name } </p>
|
|
||||||
<p> { new Date(v.completed_at).toLocaleString() } </p>
|
|
||||||
</HistoryItem>
|
</HistoryItem>
|
||||||
))
|
))
|
||||||
}
|
|
||||||
|
|
||||||
render = () => {
|
render() {
|
||||||
const { stats, version, endPointAvailable, remotes, mounts } = this.state
|
const { stats, version, endPointAvailable, remotes, mounts } = this.state
|
||||||
const { elapsedTime, transfers, bytes, errors, lastError, transferring } = stats
|
const { elapsedTime, transfers, bytes, errors, lastError, transferring } = stats
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{
|
|
||||||
// this.renderFileBrowser()
|
|
||||||
}
|
|
||||||
|
|
||||||
<HeaderContainer>
|
<HeaderContainer>
|
||||||
<LogoContainer>
|
<LogoContainer>
|
||||||
<img src="/favicon-64x64.png" alt="Rclone Dashboard" width="64" height="64" />
|
<img src="/favicon-64x64.png" alt="Rclone Dashboard" width="64" height="64" />
|
||||||
<h1> Rclone Dashboard </h1>
|
<h1>Rclone Dashboard</h1>
|
||||||
</LogoContainer>
|
</LogoContainer>
|
||||||
|
|
||||||
<StatusContainer>
|
<StatusContainer>
|
||||||
<StatusBulb style={{ background: endPointAvailable ? "var(--status-green)" : "var(--status-red)" }} />
|
<StatusBulb
|
||||||
{ endPointAvailable ? "API endpoint is behaving normally" : "API endpoint is unavailable" }
|
style={{
|
||||||
|
background: endPointAvailable ? 'var(--status-green)' : 'var(--status-red)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{endPointAvailable
|
||||||
|
? 'API endpoint is behaving normally'
|
||||||
|
: 'API endpoint is unavailable'}
|
||||||
</StatusContainer>
|
</StatusContainer>
|
||||||
</HeaderContainer>
|
</HeaderContainer>
|
||||||
|
|
||||||
@ -383,65 +269,63 @@ class App extends Component {
|
|||||||
|
|
||||||
<Container>
|
<Container>
|
||||||
<ItemsContainer>
|
<ItemsContainer>
|
||||||
<ActiveContainer>
|
<ActiveContainer>
|
||||||
<h1> Active Jobs </h1>
|
<h1>
|
||||||
{ this.renderActiveJobs() }
|
Active Jobs
|
||||||
</ActiveContainer>
|
{/* 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>
|
<HistoryContainer>
|
||||||
<h1> History </h1>
|
<h1>History</h1>
|
||||||
<HistoryItemsWrapper>
|
<HistoryItemsWrapper>{this.renderHistory()}</HistoryItemsWrapper>
|
||||||
{ this.renderHistory() }
|
|
||||||
</HistoryItemsWrapper>
|
|
||||||
</HistoryContainer>
|
</HistoryContainer>
|
||||||
</ItemsContainer>
|
</ItemsContainer>
|
||||||
|
|
||||||
<InfosContainer>
|
<InfosContainer>
|
||||||
<InfosWrapper style={{ minHeight: "10rem" }}>
|
<InfosWrapper style={{ minHeight: '10rem' }}>
|
||||||
<h2> Service Stats </h2>
|
<h2>Service Stats</h2>
|
||||||
<p> Uptime </p>
|
<p>Uptime</p>
|
||||||
<p> { secondsToTimeString(elapsedTime, true) } </p>
|
<p>{secondsToTimeString(elapsedTime, true)}</p>
|
||||||
|
<p>Speed</p>
|
||||||
<p> Speed </p>
|
<p>{this.renderLiveSpeed()}</p>
|
||||||
<p> { this.renderLiveSpeed() } </p>
|
<p>Active transfers</p>
|
||||||
|
<p>{`${(transferring || []).length} files`}</p>
|
||||||
<p> Active transfers </p>
|
<p>Total transferred files</p>
|
||||||
<p> { (transferring?.length ? transferring.length : 0) + (transferring?.length === 1 ? " file" : " files" )} </p>
|
<p>{`${transfers || 0} files`}</p>
|
||||||
|
<p>Total transferred data</p>
|
||||||
<p> Total transfered files </p>
|
<p>{bytesToString(bytes, {})}</p>
|
||||||
<p> { (transfers ? transfers : 0) + (transfers === 1 ? " file" : " files" )} </p>
|
|
||||||
|
|
||||||
<p> Total transferred data </p>
|
|
||||||
<p> { bytesToString(bytes, {}) } </p>
|
|
||||||
</InfosWrapper>
|
</InfosWrapper>
|
||||||
|
|
||||||
<InfosWrapper>
|
<InfosWrapper>
|
||||||
<h2> Environment </h2>
|
<h2>Environment</h2>
|
||||||
<p> Rclone version </p>
|
<p>Rclone version</p>
|
||||||
<p> { version.version } </p>
|
<p>{version.version}</p>
|
||||||
|
<p>GO version</p>
|
||||||
<p> GO version </p>
|
<p>{version.goVersion}</p>
|
||||||
<p> { version.goVersion } </p>
|
<p>Architecture</p>
|
||||||
|
<p>{version.arch}</p>
|
||||||
<p> Architecture </p>
|
|
||||||
<p> { version.arch } </p>
|
|
||||||
</InfosWrapper>
|
</InfosWrapper>
|
||||||
|
|
||||||
{remotes.length > 0 && (
|
{remotes.length > 0 && (
|
||||||
<InfosWrapper style={{ minHeight: "6rem" }}>
|
<InfosWrapper style={{ minHeight: '6rem' }}>
|
||||||
<h2> Remotes </h2> {/* add NEW button */}
|
<h2>Remotes</h2>
|
||||||
{ this.renderRemotes() }
|
{this.renderRemotes()}
|
||||||
</InfosWrapper>
|
</InfosWrapper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{mounts.length > 0 && (
|
{mounts.length > 0 && (
|
||||||
<InfosWrapper style={{ minHeight: "4.5rem" }}>
|
<InfosWrapper style={{ minHeight: '4.5rem' }}>
|
||||||
<h2> Mounts </h2> {/* add NEW button */}
|
<h2>Mounts</h2>
|
||||||
{ this.renderMounts() }
|
{this.renderMounts()}
|
||||||
</InfosWrapper>
|
</InfosWrapper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Settings />
|
<Settings />
|
||||||
|
|
||||||
</InfosContainer>
|
</InfosContainer>
|
||||||
</Container>
|
</Container>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -1,132 +1,114 @@
|
|||||||
import { Component, Fragment } from "react"
|
import { Component, Fragment } from 'react'
|
||||||
import { ActiveJob, ActiveTransfer, StopButton } from "../styled"
|
import { ActiveJob, ActiveTransfer, StopButton } from '../styled'
|
||||||
import API from '../utils/API'
|
import API from '../utils/API'
|
||||||
import bytesToString from "../utils/bytestring"
|
import bytesToString from '../utils/bytestring'
|
||||||
import secondsToTimeString from "../utils/timestring"
|
import secondsToTimeString from '../utils/timestring'
|
||||||
|
|
||||||
class Job extends Component {
|
class Job extends Component {
|
||||||
constructor() {
|
constructor(props) {
|
||||||
super()
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
|
jobid: props.jobid,
|
||||||
stats: {
|
stats: {
|
||||||
elapsedTime: 0,
|
elapsedTime: 0,
|
||||||
eta: 3600,
|
eta: 0,
|
||||||
bytes: 1024 * 1024 * 128,
|
bytes: 0,
|
||||||
totalBytes: 1024 * 1024 * 1024,
|
totalBytes: 0,
|
||||||
transferring: [
|
transferring: props.initialStats?.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
|
|
||||||
}
|
}
|
||||||
|
this.fetchStatsInterval = null
|
||||||
this.decreaseEtaInterval = undefined
|
this.decreaseEtaInterval = null
|
||||||
this.fetchStatsInterval = undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount() {
|
||||||
this.fetchStats()
|
if (this.state.jobid) {
|
||||||
|
this.fetchStats()
|
||||||
// make the view look more responsive by counting every second
|
this.fetchStatsInterval = setInterval(this.fetchStats, 5000)
|
||||||
if (this.decreaseEtaInterval === undefined) this.decreaseEtaInterval = setInterval(() => {
|
}
|
||||||
let { stats } = this.state
|
this.decreaseEtaInterval = setInterval(() => {
|
||||||
|
this.setState(({ stats }) => ({
|
||||||
if (stats.eta > 1) stats.eta--
|
stats: {
|
||||||
stats.elapsedTime++
|
...stats,
|
||||||
stats.transferring?.forEach(v => {if (v.eta > 1) v.eta--})
|
elapsedTime: stats.elapsedTime + 1,
|
||||||
|
eta: Math.max(0, stats.eta - 1),
|
||||||
this.setState({ stats })
|
transferring: (stats.transferring || []).map(v => ({
|
||||||
|
...v,
|
||||||
|
eta: Math.max(0, v.eta - 1)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}))
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
// get job stats every 5 seconds
|
|
||||||
if (this.fetchStatsInterval === undefined) this.fetchStatsInterval = setInterval(this.fetchStats, 5 * 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear the intervals
|
componentWillUnmount() {
|
||||||
componentWillUnmount = () => {
|
|
||||||
clearInterval(this.decreaseEtaInterval)
|
|
||||||
clearInterval(this.fetchStatsInterval)
|
clearInterval(this.fetchStatsInterval)
|
||||||
|
clearInterval(this.decreaseEtaInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fetchStats = () =>
|
||||||
* get the stats from the job this component displays
|
API.request({ url: '/core/stats', data: { group: `job/${this.state.jobid}` } })
|
||||||
*/
|
.then(({ data }) => {
|
||||||
fetchStats = () => {
|
if (typeof data === 'object') {
|
||||||
return API.request({
|
this.setState({
|
||||||
url: "/core/stats",
|
stats: {
|
||||||
data: {
|
elapsedTime: data.elapsedTime ?? 0,
|
||||||
group: "job/" + this.props.jobid
|
eta: data.eta ?? 0,
|
||||||
}
|
bytes: data.bytes ?? 0,
|
||||||
})
|
totalBytes: data.totalBytes ?? 0,
|
||||||
.then(response => {
|
transferring: Array.isArray(data.transferring) ? data.transferring : []
|
||||||
if (typeof response.data !== "object") return new Error("invalid response")
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
this.setState({ stats: response.data })
|
stopJob = () =>
|
||||||
})
|
API.request({ url: '/job/stop', data: { jobid: this.state.jobid } })
|
||||||
.catch(() => {})
|
.then(() => this.props.refreshStats())
|
||||||
}
|
.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;
|
|
||||||
|
|
||||||
|
renderActiveTransfer() {
|
||||||
|
const { transferring = [] } = this.state.stats
|
||||||
|
if (!transferring.length) return null
|
||||||
return transferring.map(v => (
|
return transferring.map(v => (
|
||||||
<ActiveTransfer key={v.name}>
|
<ActiveTransfer key={v.name}>
|
||||||
<p> { v.name } </p>
|
<p>{v.name}</p>
|
||||||
<p> { bytesToString(v.size, {}) } </p>
|
<p>{bytesToString(v.size, {})}</p>
|
||||||
<p> { secondsToTimeString(v.eta) } </p>
|
<p>{secondsToTimeString(v.eta)}</p>
|
||||||
<p> { bytesToString(v.speed, { speed: true }) } </p>
|
<p>{bytesToString(v.speed, { speed: true })}</p>
|
||||||
</ActiveTransfer>
|
</ActiveTransfer>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
render = () => {
|
render() {
|
||||||
const { stats } = this.state
|
const { stats, jobid } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
<ActiveJob>
|
||||||
|
<p>Time elapsed:</p>
|
||||||
|
<p>{secondsToTimeString(stats.elapsedTime)}</p>
|
||||||
|
|
||||||
<ActiveJob>
|
<p>Progress:</p>
|
||||||
<p> Time elapsed: </p>
|
<p>
|
||||||
<p> { secondsToTimeString(stats.elapsedTime) } </p>
|
{bytesToString(stats.bytes, { format: 'GB', fixed: 3 })} /{' '}
|
||||||
<p> Progress: </p>
|
{bytesToString(stats.totalBytes, { format: 'GB', fixed: 3 })} GB
|
||||||
<p> { bytesToString(stats.bytes, { format: "GB", fixed: 3 }) } / { bytesToString(stats.totalBytes, { format: "GB", fixed: 3 }) } GB </p>
|
</p>
|
||||||
|
|
||||||
<p> Time left: </p>
|
<p>Time left:</p>
|
||||||
<p> { secondsToTimeString(stats.eta) } </p>
|
<p>{secondsToTimeString(stats.eta)}</p>
|
||||||
<p> Progress: </p>
|
|
||||||
<p> { ((stats.bytes / stats.totalBytes) * 100 || 0).toFixed(2) } % </p>
|
|
||||||
|
|
||||||
<p> Speed: </p>
|
<p>Progress:</p>
|
||||||
<p> { bytesToString(stats.speed, { speed: true }) } </p>
|
<p>{((stats.bytes / stats.totalBytes) * 100).toFixed(2)} %</p>
|
||||||
|
|
||||||
<StopButton onClick={this.stopJob}> Cancel </StopButton>
|
<p>Speed:</p>
|
||||||
</ActiveJob>
|
<p>{bytesToString(stats.speed, { speed: true })}</p>
|
||||||
{ this.renderActiveTransfer() }
|
|
||||||
|
{jobid ? <StopButton onClick={this.stopJob}>Cancel</StopButton> : null}
|
||||||
|
</ActiveJob>
|
||||||
|
|
||||||
|
{/* Transfers only */}
|
||||||
|
{this.renderActiveTransfer()}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -141,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)`
|
export const HistoryContainer = styled(ActiveContainer)`
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
`
|
`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user