Rework the uptime page, add gauge cards, fix card with code (#3090)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-09-18 14:30:44 +02:00
1 parent 06143bf617
commit 3f767ee709
9 files changed
+331 -85

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": patch
---
Fixed the "Card with code" widget rendering its code as plain text instead of a `pre` block.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": minor
---
Added a System status panel with 60-day uptime history bars to the Uptime preview page.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": minor
---
Added ApexCharts gauge cards to the Widgets preview page.
+89 -73
View File
@@ -1,58 +1,102 @@
--- ---
import Subheader from '@ui/Subheader.astro'
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Chart from '@components/demo/Chart.astro' import Chart from '@components/demo/Chart.astro'
import Card from '@ui/Card.astro' import Card from '@ui/Card.astro'
import CardBody from '@ui/CardBody.astro' import CardBody from '@ui/CardBody.astro'
import CardHeader from '@ui/CardHeader.astro'
import CardTitle from '@ui/CardTitle.astro' import CardTitle from '@ui/CardTitle.astro'
import StatusDot from '@ui/StatusDot.astro'
import StatCard from '@components/cards/StatCard.astro'
import SystemStatus from '@components/cards/SystemStatus.astro'
const incidents = [
{ color: 'red', title: 'Email delivery outage', date: 'Aug 24, 2026', duration: '42 minutes', resolved: true },
{ color: 'yellow', title: 'Slow webhook deliveries', date: 'Aug 12, 2026', duration: '1 hour 8 minutes', resolved: true },
{ color: 'yellow', title: 'Dashboard loading slowly', date: 'Aug 3, 2026', duration: '17 minutes', resolved: true },
{ color: 'red', title: 'Email delivery outage', date: 'Jul 21, 2026', duration: '2 hours 5 minutes', resolved: true },
]
const periods = [
{ period: 'Today', availability: '100%', downtime: 'none', incidents: 0, longest: '–', average: '–' },
{ period: 'Last 7 days', availability: '100%', downtime: 'none', incidents: 0, longest: '–', average: '–' },
{ period: 'Last 30 days', availability: '99.9028%', downtime: '42 minutes', incidents: 1, longest: '42 minutes', average: '42 minutes' },
{ period: 'Last 365 days', availability: '99.9414%', downtime: '5 hours 8 minutes', incidents: 9, longest: '2 hours 5 minutes', average: '34 minutes' },
{ period: 'All time', availability: '99.9533%', downtime: '11 hours 40 minutes', incidents: 23, longest: '2 hours 5 minutes', average: '30 minutes' },
]
--- ---
<DefaultLayout title="Uptime" pageHeaderFile="uptime" pageMenu="extra.uptime" pageLibs={['apexcharts']}> <DefaultLayout title="Uptime" pageHeaderFile="uptime" pageMenu="extra.uptime" pageLibs={['apexcharts']}>
<div class="row row-cards"> <div class="row row-cards">
<div class="col-md-4"> <div class="col-sm-6 col-lg-3">
<StatCard title="30-day uptime" value="99.9%" icon="activity" />
</div>
<div class="col-sm-6 col-lg-3">
<StatCard title="Currently up for" value="14d 2h 54m" icon="clock" />
</div>
<div class="col-sm-6 col-lg-3">
<StatCard title="Avg. response time" value="142 ms" icon="bolt" />
</div>
<div class="col-sm-6 col-lg-3">
<StatCard title="30-day incidents" value="1" icon="alert-triangle" />
</div>
<div class="col-lg-8">
<SystemStatus id="services" class="h-100" />
</div>
<div class="col-lg-4">
<Card class="h-100">
<CardHeader>
<CardTitle>Recent incidents</CardTitle>
</CardHeader>
<div class="list-group list-group-flush">
{
incidents.map((incident) => (
<div class="list-group-item">
<div class="row align-items-center">
<div class="col-auto">
<StatusDot color={incident.color} />
</div>
<div class="col text-truncate">
<div class="fw-medium text-truncate">{incident.title}</div>
<div class="text-secondary text-truncate">
{incident.date} · {incident.duration}
</div>
</div>
<div class="col-auto">
<span class="badge bg-green-lt">Resolved</span>
</div>
</div>
</div>
))
}
</div>
<div class="card-footer mt-auto">
<a href="#">View all incidents</a>
</div>
</Card>
</div>
<div class="col-lg-8">
<Card> <Card>
<CardBody> <CardBody>
<Subheader>Currently up for</Subheader> <CardTitle>Response time in the last 4 months (ms)</CardTitle>
<div class="h3 m-0">14 days 2 hours 54 mins 34 seconds</div> <Chart chartId="uptime" height={15} label="Response time in the last 4 months" />
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-md-4"> <div class="col-lg-4">
<Card> <Card>
<CardBody> <CardBody>
<Subheader>Last checked at</Subheader> <CardTitle>Incidents per day</CardTitle>
<div class="h3 m-0">27 seconds ago</div> <Chart chartId="uptime-incidents" height={15} label="Incidents per day" />
</CardBody>
</Card>
</div>
<div class="col-md-4">
<Card>
<CardBody>
<Subheader>Incidents</Subheader>
<div class="h3 m-0">3</div>
</CardBody>
</Card>
</div>
<div class="col-md-8">
<Card>
<CardBody>
<CardTitle>Response times across regions in the last day</CardTitle>
<Chart chartId="uptime" height={15} label="Response times across regions in the last day" />
</CardBody>
</Card>
</div>
<div class="col-md-4">
<Card>
<CardBody>
<CardTitle>Uptime incidents per day</CardTitle>
<Chart chartId="uptime-incidents" height={15} label="Uptime incidents per day" />
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardHeader>
<CardTitle>Availability</CardTitle>
</CardHeader>
<div class="card-table table-responsive"> <div class="card-table table-responsive">
<table class="table"> <table class="table table-vcenter">
<thead> <thead>
<tr> <tr>
<th>Time period</th> <th>Time period</th>
@@ -63,47 +107,19 @@ import CardTitle from '@ui/CardTitle.astro'
<th>Avg. incident</th> <th>Avg. incident</th>
</tr> </tr>
</thead> </thead>
<tbody <tbody>
><tr> {
<td>Today</td> periods.map((row) => (
<td>98.9533%</td> <tr>
<td>1 minute</td> <td>{row.period}</td>
<td>1</td> <td class="fw-medium">{row.availability}</td>
<td>1 minute</td> <td class="text-secondary">{row.downtime}</td>
<td>1 minute</td> <td class="text-secondary">{row.incidents}</td>
</tr> <td class="text-secondary">{row.longest}</td>
<tr> <td class="text-secondary">{row.average}</td>
<td>Last 7 days</td> </tr>
<td>98.9533%</td> ))
<td>1 minute</td> }
<td>1</td>
<td>1 minute</td>
<td>1 minute</td>
</tr>
<tr>
<td>Last 30 days</td>
<td>98.9533%</td>
<td>1 minute</td>
<td>1</td>
<td>1 minute</td>
<td>1 minute</td>
</tr>
<tr>
<td>Last 365 days</td>
<td>98.9533%</td>
<td>1 minute</td>
<td>1</td>
<td>1 minute</td>
<td>1 minute</td>
</tr>
<tr>
<td>All time</td>
<td>98.9533%</td>
<td>1 minute</td>
<td>1</td>
<td>1 minute</td>
<td>1 minute</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
+11 -1
View File
@@ -12,9 +12,10 @@ import ProjectSummary from '@shared/components/cards/ProjectSummary.astro'
import ProjectKanban from '@shared/components/cards/ProjectKanban.astro' import ProjectKanban from '@shared/components/cards/ProjectKanban.astro'
import UserInfo from '@shared/components/cards/UserInfo.astro' import UserInfo from '@shared/components/cards/UserInfo.astro'
import Configuration from '@shared/components/cards/Configuration.astro' import Configuration from '@shared/components/cards/Configuration.astro'
import Gauge from '@shared/components/cards/Gauge.astro'
--- ---
<DefaultLayout title="Widgets" pageMenu="extra.widgets"> <DefaultLayout title="Widgets" pageMenu="extra.widgets" pageLibs={['apexcharts']}>
<div class="row row-cards"> <div class="row row-cards">
<div class="col-12 col-md-6 col-xxl-3 mb-4 mb-xxl-0"> <div class="col-12 col-md-6 col-xxl-3 mb-4 mb-xxl-0">
<StatCard title="Earned" value="$2,847" icon="credit-card" countup /> <StatCard title="Earned" value="$2,847" icon="credit-card" countup />
@@ -29,6 +30,15 @@ import Configuration from '@shared/components/cards/Configuration.astro'
<StatCard title="Weekly growth" value="18.4%" icon="discount" countup /> <StatCard title="Weekly growth" value="18.4%" icon="discount" countup />
</div> </div>
<div class="col-md-4">
<Gauge id="deliverability" title="Deliverability" value={96} label="delivered" count={48210} total={50219} color="green" description="Share of sent emails accepted by the receiving server. Anything above 95% is healthy." />
</div>
<div class="col-md-4">
<Gauge id="storage" title="Storage used" value={82} label="used" count={410} total={500} color="yellow" description="Disk space taken by uploads and backups, in GB. Consider an upgrade once it passes 80%." />
</div>
<div class="col-md-4">
<Gauge id="onboarding" title="Onboarding" value={58} label="completed" count={1392} total={2400} description="New accounts that finished every setup step. There is no target for this metric yet." />
</div>
<div class="col-lg-6"> <div class="col-lg-6">
<ProjectProgress /> <ProjectProgress />
</div> </div>
+10 -10
View File
@@ -1,21 +1,21 @@
--- ---
// Raw CSS text inside .card-code (no highlight markup). // Plain <pre><code> inside .card-code (no highlight markup).
import CardTitle from '@ui/CardTitle.astro' import CardTitle from '@ui/CardTitle.astro'
const code = ` const code = `.card-footer {
.card-footer { background: transparent;
background: transparent;
&:last-child { &:last-child {
border-radius: 0 0 1 2; border-radius: 0 0 4px 4px;
} }
} }`
`
--- ---
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<CardTitle>Card with code</CardTitle> <CardTitle>Card with code</CardTitle>
</div> </div>
<div class="card-code">{code}</div> <div class="card-code">
<pre><code>{code}</code></pre>
</div>
</div> </div>
+91
View File
@@ -0,0 +1,91 @@
---
import CaptureScript from '../CaptureScript.astro'
interface Props {
id: string
title: string
description: string
value: number
label: string
count: number
total: number
countLabel?: string
color?: 'green' | 'yellow' | 'neutral'
}
const { id, title, description, value, label, count, total, countLabel = 'of', color = 'neutral' } = Astro.props
// Needs the `apexcharts` lib on the page (pageLibs / docs-libs).
const chartId = `chart-gauge-${id}`
const chartColor = color === 'neutral' ? 'var(--tblr-body-color)' : `var(--tblr-${color})`
const format = (n: number) => n.toLocaleString('en-US')
---
<div class="card">
<div class="card-body">
<h3 class="card-title">{title}</h3>
<div id={chartId} class="position-relative mx-auto" style="width: 10rem; max-width: 100%" role="img" aria-label={`${title}: ${value}%`}></div>
<p class="text-secondary mt-3 mb-0" style="min-height: 2lh">{description}</p>
</div>
<div class="card-footer d-flex text-secondary">
<div><span class="fw-medium text-body">{format(count)}</span> {countLabel} {format(total)}</div>
<div class="ms-auto">{format(total - count)} remaining</div>
</div>
</div>
<CaptureScript>
<!-- BEGIN GAUGE -->
<script is:inline define:vars={{ chartId, value, label, chartColor }}>
function initGauge() {
window.tabler_chart ??= {}
window.ApexCharts &&
(window.tabler_chart[chartId] = new ApexCharts(document.getElementById(chartId), {
series: [value],
chart: {
type: 'radialBar',
height: 160,
sparkline: {
enabled: true,
},
},
plotOptions: {
radialBar: {
hollow: {
size: '64%',
},
track: {
background: 'var(--tblr-border-color)',
},
dataLabels: {
name: {
show: true,
offsetY: 18,
fontSize: '12px',
fontFamily: 'inherit',
fontWeight: 400,
color: 'var(--tblr-secondary)',
},
value: {
show: true,
offsetY: -12,
fontSize: '24px',
fontFamily: 'inherit',
fontWeight: 500,
color: 'var(--tblr-body-color)',
formatter: (val) => `${val}%`,
},
},
},
},
colors: [chartColor],
stroke: {
lineCap: 'round',
},
labels: [label],
})).render()
}
document.readyState !== 'loading' ? initGauge() : document.addEventListener('DOMContentLoaded', initGauge, { once: true })
</script>
<!-- END GAUGE -->
</CaptureScript>
+114
View File
@@ -0,0 +1,114 @@
---
import CaptureScript from '../CaptureScript.astro'
interface Props {
id: string
services?: string[]
days?: number
seed?: number
class?: string
}
const { id, services = ['API', 'Dashboard', 'Webhooks', 'Email delivery'], days = 60, seed = 18, class: className } = Astro.props
const cardId = `system-status-${id}`
---
<div class:list={['card', className]} id={cardId}>
<div class="card-header">
<h3 class="card-title">System status</h3>
<div class="card-actions">
<span class="status status-green" data-status>
<span class="status-dot status-dot-animated"></span>
<span data-status-label>All systems operational</span>
</span>
</div>
</div>
<div class="list-group list-group-flush">
{
services.map((service) => (
<div class="list-group-item">
<div class="d-flex align-items-baseline mb-2">
<div class="fw-medium">{service}</div>
<div class="ms-auto text-secondary">
<span data-uptime-value>–</span> uptime
</div>
</div>
<div class="tracking" data-uptime={service} />
</div>
))
}
</div>
<div class="card-footer">
<div class="d-flex align-items-center text-secondary">
<div>{days} days ago</div>
<div class="legend-list mx-auto">
<span class="legend legend-green"><span class="legend-dot"></span>Operational</span>
<span class="legend legend-yellow"><span class="legend-dot"></span>Degraded</span>
<span class="legend legend-red"><span class="legend-dot"></span>Outage</span>
</div>
<div>Today</div>
</div>
</div>
</div>
<CaptureScript>
<!-- BEGIN SYSTEM STATUS -->
<script is:inline define:vars={{ cardId, days, seed }}>
// Share of days with an outage / degraded performance, and how much a degraded day counts
const OUTAGE_RATE = 0.02
const DEGRADED_RATE = 0.04
const DEGRADED_WEIGHT = 0.5
const STATUSES = {
up: { label: 'Operational', color: 'bg-green' },
degraded: { label: 'Degraded performance', color: 'bg-yellow' },
down: { label: 'Outage', color: 'bg-red' },
}
// Mulberry32: tiny seeded PRNG, so every load draws the same history
function createRandom(seed) {
return function () {
seed = (seed + 0x6d2b79f5) | 0
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
function initSystemStatus() {
const card = document.getElementById(cardId)
const random = createRandom(seed)
const today = new Date()
let allOperational = true
card.querySelectorAll('[data-uptime]').forEach((container) => {
let score = 0
for (let i = days - 1; i >= 0; i--) {
const roll = random()
const status = roll < OUTAGE_RATE ? 'down' : roll < OUTAGE_RATE + DEGRADED_RATE ? 'degraded' : 'up'
const date = new Date(today.getFullYear(), today.getMonth(), today.getDate() - i)
const bar = document.createElement('div')
bar.className = 'tracking-block ' + STATUSES[status].color
bar.title = date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) + ' – ' + STATUSES[status].label
container.appendChild(bar)
score += status === 'up' ? 1 : status === 'degraded' ? DEGRADED_WEIGHT : 0
}
if (score < days) allOperational = false
container.parentElement.querySelector('[data-uptime-value]').textContent = ((score / days) * 100).toFixed(2) + '%'
})
if (!allOperational) {
card.querySelector('[data-status]').classList.replace('status-green', 'status-yellow')
card.querySelector('[data-status-label]').textContent = 'Some systems affected'
}
}
document.readyState !== 'loading' ? initSystemStatus() : document.addEventListener('DOMContentLoaded', initSystemStatus, { once: true })
</script>
<!-- END SYSTEM STATUS -->
</CaptureScript>
+1 -1
View File
@@ -6,7 +6,7 @@ import PageTitle from '@ui/PageTitle.astro'
<!-- BEGIN HEADER UPTIME --> <!-- BEGIN HEADER UPTIME -->
<div class="page-header d-print-none"> <div class="page-header d-print-none">
<div class="container"> <div class="container-xl">
<div class="row g-3 align-items-center"> <div class="row g-3 align-items-center">
<div class="col-auto"> <div class="col-auto">
<span class="status-indicator status-green status-indicator-animated"> <span class="status-indicator status-green status-indicator-animated">