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:
+173
-136
@@ -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}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user