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