337 lines
9.1 KiB
JavaScript
337 lines
9.1 KiB
JavaScript
import { Component, Fragment } from 'react'
|
|
import ReactTooltip from 'react-tooltip'
|
|
|
|
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'
|
|
import bytesToString from './utils/bytestring'
|
|
|
|
import Settings from './components/settings'
|
|
import Job from './components/job'
|
|
import Navigation from './components/navigation'
|
|
|
|
class App extends Component {
|
|
constructor() {
|
|
super()
|
|
this.state = {
|
|
stats: {
|
|
elapsedTime: 0,
|
|
transfers: 0,
|
|
bytes: 0,
|
|
errors: 0,
|
|
lastError: '',
|
|
transferring: [],
|
|
checking: []
|
|
},
|
|
remotes: [],
|
|
mounts: [],
|
|
transferred: [], // history
|
|
version: {},
|
|
endPointAvailable: true
|
|
}
|
|
this.infoInterval = null
|
|
this.apiInterval = null
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.fetchRemotes()
|
|
this.fetchMounts()
|
|
this.fetchVersionInfo()
|
|
this.fetchInfos()
|
|
this.infoInterval = setInterval(this.fetchInfos, 2000)
|
|
this.apiInterval = setInterval(this.checkApiEndpoint, 5000)
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
clearInterval(this.infoInterval)
|
|
clearInterval(this.apiInterval)
|
|
}
|
|
|
|
/**
|
|
* check if the api is still available
|
|
* if state changes show this to the user
|
|
* also updates the favicon
|
|
*/
|
|
checkApiEndpoint = () => {
|
|
const status = API.getEndpointStatus()
|
|
if (status !== this.state.endPointAvailable) {
|
|
this.setState({ endPointAvailable: status })
|
|
if (status) {
|
|
this.fetchRemotes()
|
|
this.fetchMounts()
|
|
this.fetchVersionInfo()
|
|
}
|
|
}
|
|
}
|
|
|
|
fetchRemotes = () => {
|
|
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 }))
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
|
|
fetchMounts = () => {
|
|
API.request({ url: '/mount/listmounts' })
|
|
.then(res => {
|
|
if (!Array.isArray(res.data.mountPoints)) throw new Error()
|
|
this.setState({ mounts: res.data.mountPoints })
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
|
|
fetchVersionInfo = () => {
|
|
API.request({ url: '/core/version' })
|
|
.then(res => {
|
|
if (typeof res.data !== 'object') throw new Error()
|
|
this.setState({ version: res.data })
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
|
|
fetchInfos = () => {
|
|
API.request({ url: '/core/stats' })
|
|
.then(({ data: stats }) => {
|
|
if (typeof stats !== 'object') throw new Error()
|
|
|
|
// store latest stats
|
|
this.setState({ stats })
|
|
|
|
// 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(() => {})
|
|
}
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
|
|
renderLiveSpeed = () => {
|
|
const list = Array.isArray(this.state.stats.transferring)
|
|
? this.state.stats.transferring
|
|
: []
|
|
let speed = 0
|
|
list.forEach(item => {
|
|
speed += typeof item?.speed === 'number' ? item.speed : 0
|
|
})
|
|
return bytesToString(speed, { speed: true })
|
|
}
|
|
|
|
renderRemotes = () => {
|
|
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>
|
|
))
|
|
}
|
|
|
|
renderMounts = () => {
|
|
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>
|
|
))
|
|
}
|
|
|
|
renderActiveJobs = () => {
|
|
const { transferring = [], checking = [] } = this.state.stats
|
|
// collapse if nothing
|
|
if (!transferring.length && !checking.length) 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>
|
|
}
|
|
|
|
// otherwise show transfers then checks
|
|
return (
|
|
<Fragment>
|
|
{transferJobs}
|
|
{checkingRows}
|
|
</Fragment>
|
|
)
|
|
}
|
|
|
|
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, remotes, mounts } = this.state
|
|
const { elapsedTime, transfers, bytes, errors, lastError, transferring } = stats
|
|
|
|
return (
|
|
<Fragment>
|
|
<HeaderContainer>
|
|
<LogoContainer>
|
|
<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'}
|
|
</StatusContainer>
|
|
</HeaderContainer>
|
|
|
|
<Navigation info={{ errors, lastError }} />
|
|
|
|
<Container>
|
|
<ItemsContainer>
|
|
<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>
|
|
</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} 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>
|
|
</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>
|
|
)
|
|
}
|
|
}
|
|
|
|
export default App
|