Implement tab navigation and health check display in DashboardPage

- Added tab navigation to switch between 'Overview' and 'Health Check' sections.
- Introduced health issues count badge in the 'Health Check' tab.
- Refactored the layout to display account balances and upcoming payment expirations in a structured format.
- Enhanced user interface for better visibility of account statuses and health metrics.
This commit is contained in:
Denozordec
2026-03-20 23:21:00 +07:00
parent 93cb6422d5
commit 82ad7c633a
+173 -136
View File
@@ -17,6 +17,7 @@ import {
} from '@tabler/icons-react' } from '@tabler/icons-react'
export function DashboardPage({ db = {}, settings, ratesData }) { export function DashboardPage({ db = {}, settings, ratesData }) {
const [dashTab, setDashTab] = useState('overview')
const vps = Array.isArray(db.vps) ? db.vps : [] const vps = Array.isArray(db.vps) ? db.vps : []
const providerAccounts = Array.isArray(db.providerAccounts) ? db.providerAccounts : [] const providerAccounts = Array.isArray(db.providerAccounts) ? db.providerAccounts : []
const balanceLedger = Array.isArray(db.balanceLedger) ? db.balanceLedger : [] const balanceLedger = Array.isArray(db.balanceLedger) ? db.balanceLedger : []
@@ -234,85 +235,37 @@ export function DashboardPage({ db = {}, settings, ratesData }) {
.sort((a, b) => a.paidUntil - b.paidUntil) .sort((a, b) => a.paidUntil - b.paidUntil)
.slice(0, 10) .slice(0, 10)
const healthIssuesCount = inventoryIssues.reduce((n, i) => n + (i.count || 0), 0)
return ( return (
<> <>
<PageHeader pretitle="Обзор" title="Дашборд" /> <PageHeader pretitle="Обзор" title="Дашборд" />
<div className="row row-cards"> <ul className="nav nav-tabs mb-3" role="tablist">
{inventoryIssues.length > 0 ? ( <li className="nav-item" role="presentation">
<div className="col-12"> <button
<div className="card border-warning"> type="button"
<div className="card-header"> className={`nav-link ${dashTab === 'overview' ? 'active' : ''}`}
<h3 className="card-title">Здоровье инвентаря</h3> onClick={() => setDashTab('overview')}
</div> >
<div className="card-body"> Обзор
<div className="row g-2"> </button>
{inventoryIssues.map((issue) => ( </li>
<div className="col-md-6 col-xl-4" key={issue.key}> <li className="nav-item" role="presentation">
<Link className="card card-link" to={issue.to}> <button
<div className="card-body py-2 px-3"> type="button"
<div className="d-flex justify-content-between align-items-center"> className={`nav-link ${dashTab === 'health' ? 'active' : ''}`}
<span className="fw-medium">{issue.title}</span> onClick={() => setDashTab('health')}
<span className="badge bg-orange-lt text-orange">{issue.count}</span> >
</div> Health Check
{issue.hint ? <div className="text-secondary small mt-1">{issue.hint}</div> : null} {inventoryIssues.length > 0 ? (
</div> <span className="badge bg-orange-lt text-orange ms-2">{healthIssuesCount}</span>
</Link>
</div>
))}
</div>
</div>
</div>
</div>
) : null}
<div className="col-12">
<div className="card">
<div className="card-header">
<h3 className="card-title">Последние синхронизации</h3>
<div className="card-actions">
<Link to="/accounts" className="btn btn-sm btn-outline-secondary">
Аккаунты
</Link>
</div>
</div>
<div className="list-group list-group-flush">
{recentSyncFeed.map((row) => {
const acc = providerAccounts.find((a) => a.id === row.accountId)
const line = formatSyncSummaryLine(row.summary)
return (
<div key={row.id} className="list-group-item">
<div className="d-flex justify-content-between align-items-start gap-2">
<div>
<div className="fw-medium">{acc?.name || row.accountId}</div>
<div className="text-secondary small">
{row.status === 'error' ? (
<span className="text-danger">{row.error || line}</span>
) : (
line || 'OK'
)}
</div>
</div>
<span className={`badge ${row.status === 'ok' ? 'bg-green-lt text-green' : 'bg-red-lt text-red'}`}>
{row.status === 'ok' ? 'OK' : 'Ошибка'}
</span>
</div>
<div className="text-secondary small mt-1">
{row.finishedAt
? new Date(row.finishedAt).toLocaleString('ru-RU')
: '—'}
</div>
</div>
)
})}
{recentSyncFeed.length === 0 ? (
<div className="list-group-item text-secondary text-center py-4">
Запустите синхронизацию на странице аккаунтов здесь появится краткий итог
</div>
) : null} ) : null}
</div> </button>
</div> </li>
</div> </ul>
{dashTab === 'overview' ? (
<div className="row row-cards">
<div className="col-sm-6 col-lg-3"> <div className="col-sm-6 col-lg-3">
<div className="card metric-card metric-blue h-100"> <div className="card metric-card metric-blue h-100">
<div className="card-body"> <div className="card-body">
@@ -372,6 +325,79 @@ export function DashboardPage({ db = {}, settings, ratesData }) {
</div> </div>
</div> </div>
<div className="col-12 col-xl-8">
<div className="card">
<div className="card-header">
<h3 className="card-title">Остатки по аккаунтам хостеров</h3>
</div>
<div className="table-responsive">
<table className="table card-table table-vcenter">
<thead>
<tr>
<th>Аккаунт</th>
<th>Валюта</th>
<th>Баланс</th>
</tr>
</thead>
<tbody>
{accountBalances.map((item) => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.currency}</td>
<td>
<ConvertedAmount
amount={item.balance}
currency={item.currency}
provider={db.providers.find((provider) => provider.id === item.providerId)}
settings={settings}
ratesData={ratesData}
/>
</td>
</tr>
))}
{accountBalances.length === 0 ? (
<EmptyState message="Пока нет аккаунтов" colSpan={3} />
) : null}
</tbody>
</table>
</div>
</div>
</div>
<div className="col-12 col-xl-4">
<div className="card">
<div className="card-header">
<h3 className="card-title">Истекающая оплата (ближайшие {UPCOMING_DAYS} дней)</h3>
</div>
<div className="list-group list-group-flush">
{upcoming.map(({ vps: item, paidUntil }) => {
const provider = providers.find((p) => p.id === item.providerId)
const account = providerAccounts.find((a) => a.id === item.providerAccountId)
return (
<div key={item.id} className="list-group-item">
<div className="d-flex justify-content-between">
<div>
<div className="fw-medium">{item.dns || item.ip}</div>
<div className="text-secondary small">
{provider?.name || '—'} / {account?.name || '—'}
</div>
</div>
<div className="text-secondary">
{paidUntil.toLocaleDateString('ru-RU')}
</div>
</div>
</div>
)
})}
{upcoming.length === 0 ? (
<div className="list-group-item text-secondary text-center py-4">
Нет VPS с истекающей оплатой в ближайшие {UPCOMING_DAYS} дней
</div>
) : null}
</div>
</div>
</div>
<div className="col-12 col-xl-6"> <div className="col-12 col-xl-6">
<div className="card"> <div className="card">
<div className="card-header"> <div className="card-header">
@@ -463,80 +489,91 @@ export function DashboardPage({ db = {}, settings, ratesData }) {
</div> </div>
</div> </div>
</div> </div>
</div>
) : null}
<div className="col-12 col-xl-7"> {dashTab === 'health' ? (
<div className="card"> <div className="row row-cards">
<div className="card-header"> <div className="col-12">
<h3 className="card-title">Остатки по аккаунтам хостеров</h3> <div className={`card ${inventoryIssues.length > 0 ? 'border-warning' : ''}`}>
</div> <div className="card-header">
<div className="table-responsive"> <h3 className="card-title">Health Check</h3>
<table className="table card-table table-vcenter"> </div>
<thead> <div className="card-body">
<tr> {inventoryIssues.length > 0 ? (
<th>Аккаунт</th> <div className="row g-2">
<th>Валюта</th> {inventoryIssues.map((issue) => (
<th>Баланс</th> <div className="col-md-6 col-xl-4" key={issue.key}>
</tr> <Link className="card card-link" to={issue.to}>
</thead> <div className="card-body py-2 px-3">
<tbody> <div className="d-flex justify-content-between align-items-center">
{accountBalances.map((item) => ( <span className="fw-medium">{issue.title}</span>
<tr key={item.id}> <span className="badge bg-orange-lt text-orange">{issue.count}</span>
<td>{item.name}</td> </div>
<td>{item.currency}</td> {issue.hint ? <div className="text-secondary small mt-1">{issue.hint}</div> : null}
<td> </div>
<ConvertedAmount </Link>
amount={item.balance} </div>
currency={item.currency} ))}
provider={db.providers.find((provider) => provider.id === item.providerId)} </div>
settings={settings} ) : (
ratesData={ratesData} <div className="text-secondary text-center py-4">
/> Замечаний по инвентарю нет. При появлении проблем они отобразятся здесь со ссылками на списки.
</td> </div>
</tr> )}
))} </div>
{accountBalances.length === 0 ? (
<EmptyState message="Пока нет аккаунтов" colSpan={3} />
) : null}
</tbody>
</table>
</div> </div>
</div> </div>
</div>
<div className="col-12 col-xl-5"> <div className="col-12">
<div className="card"> <div className="card">
<div className="card-header"> <div className="card-header">
<h3 className="card-title">Истекающая оплата (ближайшие {UPCOMING_DAYS} дней)</h3> <h3 className="card-title">Последние синхронизации</h3>
</div> <div className="card-actions">
<div className="list-group list-group-flush"> <Link to="/accounts" className="btn btn-sm btn-outline-secondary">
{upcoming.map(({ vps: item, paidUntil }) => { Аккаунты
const provider = providers.find((p) => p.id === item.providerId) </Link>
const account = providerAccounts.find((a) => a.id === item.providerAccountId) </div>
return ( </div>
<div key={item.id} className="list-group-item"> <div className="list-group list-group-flush">
<div className="d-flex justify-content-between"> {recentSyncFeed.map((row) => {
<div> const acc = providerAccounts.find((a) => a.id === row.accountId)
<div className="fw-medium">{item.dns || item.ip}</div> const line = formatSyncSummaryLine(row.summary)
<div className="text-secondary small"> return (
{provider?.name || '—'} / {account?.name || '—'} <div key={row.id} className="list-group-item">
<div className="d-flex justify-content-between align-items-start gap-2">
<div>
<div className="fw-medium">{acc?.name || row.accountId}</div>
<div className="text-secondary small">
{row.status === 'error' ? (
<span className="text-danger">{row.error || line}</span>
) : (
line || 'OK'
)}
</div>
</div> </div>
<span className={`badge ${row.status === 'ok' ? 'bg-green-lt text-green' : 'bg-red-lt text-red'}`}>
{row.status === 'ok' ? 'OK' : 'Ошибка'}
</span>
</div> </div>
<div className="text-secondary"> <div className="text-secondary small mt-1">
{paidUntil.toLocaleDateString('ru-RU')} {row.finishedAt
? new Date(row.finishedAt).toLocaleString('ru-RU')
: '—'}
</div> </div>
</div> </div>
)
})}
{recentSyncFeed.length === 0 ? (
<div className="list-group-item text-secondary text-center py-4">
Запустите синхронизацию на странице аккаунтов здесь появится краткий итог
</div> </div>
) ) : null}
})} </div>
{upcoming.length === 0 ? (
<div className="list-group-item text-secondary text-center py-4">
Нет VPS с истекающей оплатой в ближайшие {UPCOMING_DAYS} дней
</div>
) : null}
</div> </div>
</div> </div>
</div> </div>
</div> ) : null}
</> </>
) )
} }