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 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'
|
||||
@ -10,105 +26,41 @@ import bytesToString from './utils/bytestring'
|
||||
import Settings from './components/settings'
|
||||
import Job from './components/job'
|
||||
import Navigation from './components/navigation'
|
||||
//import FileBrowserMenu from './components/fileBrowserMenu'
|
||||
|
||||
class App extends Component {
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {
|
||||
stats: {
|
||||
// bytes: 0,
|
||||
//checking: [
|
||||
// "a very very very long absurdly unneccesarily long name this is if you cant tell",
|
||||
// "a very very very long absurdly unneccesarily long name this is if you cant tell 2"
|
||||
//]
|
||||
// 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,
|
||||
// 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,
|
||||
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"
|
||||
},
|
||||
endPointAvailable: true,
|
||||
renderBrowser: false
|
||||
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 1 seconds
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
@ -120,21 +72,7 @@ class App extends Component {
|
||||
checkApiEndpoint = () => {
|
||||
const status = API.getEndpointStatus()
|
||||
if (status !== 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 (status) {
|
||||
segments[0] = segments[0].substring(0, segments[0].length - 3)
|
||||
} else {
|
||||
segments[0] += "-gs"
|
||||
}
|
||||
v.href = segments.join(".")
|
||||
})
|
||||
|
||||
// get info after api recovers
|
||||
this.setState({ endPointAvailable: status })
|
||||
if (status) {
|
||||
this.fetchRemotes()
|
||||
this.fetchMounts()
|
||||
@ -143,239 +81,187 @@ class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = () => {
|
||||
const { remotes } = this.state
|
||||
if (remotes.length === 0) return null
|
||||
|
||||
return remotes.map(v => (
|
||||
if (!remotes.length) return null
|
||||
return remotes.map(r => (
|
||||
<InfosRow
|
||||
key={"mount" + v.name}
|
||||
data-tip={bytesToString(v.bytes, { fixed: 2 })}
|
||||
data-for={"size"+v.MountPoint}>
|
||||
|
||||
<p>{v.name}</p>
|
||||
<p>{v.type}</p>
|
||||
{/* add EDIT button */}
|
||||
<ReactTooltip id={"size"+v.MountPoint} place="left" type="info" effect="solid" globalEventOff="click" />
|
||||
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 = () => {
|
||||
const { mounts } = this.state
|
||||
if (mounts.length === 0) return null
|
||||
|
||||
return mounts.map(v => (
|
||||
<Fragment key={v.MountPoint}>
|
||||
<p>{v.Fs}</p>
|
||||
<p>{v.MountPoint}</p>
|
||||
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 = () => {
|
||||
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
|
||||
if (!Array.isArray(transferring) || transferring.length === 0) {
|
||||
return null
|
||||
// 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
|
||||
}, {})
|
||||
|
||||
// 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
|
||||
const activeJobIds = Array.from(
|
||||
new Set(
|
||||
transferring
|
||||
.map(v => v.group)
|
||||
.filter(g => typeof g === 'string')
|
||||
)
|
||||
// otherwise show transfers then checks
|
||||
return (
|
||||
<Fragment>
|
||||
{transferJobs}
|
||||
{checkingRows}
|
||||
</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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = () => {
|
||||
render() {
|
||||
const { stats, version, endPointAvailable, remotes, mounts } = this.state
|
||||
const { elapsedTime, transfers, bytes, errors, lastError, transferring } = stats
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
// this.renderFileBrowser()
|
||||
}
|
||||
|
||||
<HeaderContainer>
|
||||
<LogoContainer>
|
||||
<img src="/favicon-64x64.png" alt="Rclone Dashboard" width="64" height="64" />
|
||||
<h1> Rclone Dashboard </h1>
|
||||
<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>
|
||||
|
||||
@ -383,65 +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> 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 ? transferring.length : 0) + (transferring?.length === 1 ? " file" : " files" )} </p>
|
||||
|
||||
<p> Total transfered files </p>
|
||||
<p> { (transfers ? transfers : 0) + (transfers === 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>
|
||||
<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>
|
||||
<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> {/* add NEW button */}
|
||||
{ this.renderRemotes() }
|
||||
<InfosWrapper style={{ minHeight: '6rem' }}>
|
||||
<h2>Remotes</h2>
|
||||
{this.renderRemotes()}
|
||||
</InfosWrapper>
|
||||
)}
|
||||
|
||||
{mounts.length > 0 && (
|
||||
<InfosWrapper style={{ minHeight: "4.5rem" }}>
|
||||
<h2> Mounts </h2> {/* add NEW button */}
|
||||
{ this.renderMounts() }
|
||||
<InfosWrapper style={{ minHeight: '4.5rem' }}>
|
||||
<h2>Mounts</h2>
|
||||
{this.renderMounts()}
|
||||
</InfosWrapper>
|
||||
)}
|
||||
|
||||
<Settings />
|
||||
|
||||
</InfosContainer>
|
||||
</Container>
|
||||
</Fragment>
|
||||
|
@ -1,132 +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>
|
||||
|
||||
<p> Speed: </p>
|
||||
<p> { bytesToString(stats.speed, { speed: true }) } </p>
|
||||
<p>Progress:</p>
|
||||
<p>{((stats.bytes / stats.totalBytes) * 100).toFixed(2)} %</p>
|
||||
|
||||
<StopButton onClick={this.stopJob}> Cancel </StopButton>
|
||||
</ActiveJob>
|
||||
{ this.renderActiveTransfer() }
|
||||
<p>Speed:</p>
|
||||
<p>{bytesToString(stats.speed, { speed: true })}</p>
|
||||
|
||||
{jobid ? <StopButton onClick={this.stopJob}>Cancel</StopButton> : null}
|
||||
</ActiveJob>
|
||||
|
||||
{/* Transfers only */}
|
||||
{this.renderActiveTransfer()}
|
||||
</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)`
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
Loading…
x
Reference in New Issue
Block a user