mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Rework the uptime page, add gauge cards, fix card with code (#3090)
This commit is contained in:
1 parent
06143bf617
commit
3f767ee709
9 files changed
+331
-85
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Fixed the "Card with code" widget rendering its code as plain text instead of a `pre` block.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Added a System status panel with 60-day uptime history bars to the Uptime preview page.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Added ApexCharts gauge cards to the Widgets preview page.
|
||||
+89
-73
@@ -1,58 +1,102 @@
|
||||
---
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import Chart from '@components/demo/Chart.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardHeader from '@ui/CardHeader.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']}>
|
||||
<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>
|
||||
<CardBody>
|
||||
<Subheader>Currently up for</Subheader>
|
||||
<div class="h3 m-0">14 days 2 hours 54 mins 34 seconds</div>
|
||||
<CardTitle>Response time in the last 4 months (ms)</CardTitle>
|
||||
<Chart chartId="uptime" height={15} label="Response time in the last 4 months" />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Subheader>Last checked at</Subheader>
|
||||
<div class="h3 m-0">27 seconds ago</div>
|
||||
</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" />
|
||||
<CardTitle>Incidents per day</CardTitle>
|
||||
<Chart chartId="uptime-incidents" height={15} label="Incidents per day" />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Availability</CardTitle>
|
||||
</CardHeader>
|
||||
<div class="card-table table-responsive">
|
||||
<table class="table">
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time period</th>
|
||||
@@ -63,47 +107,19 @@ import CardTitle from '@ui/CardTitle.astro'
|
||||
<th>Avg. incident</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
><tr>
|
||||
<td>Today</td>
|
||||
<td>98.9533%</td>
|
||||
<td>1 minute</td>
|
||||
<td>1</td>
|
||||
<td>1 minute</td>
|
||||
<td>1 minute</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last 7 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 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>
|
||||
{
|
||||
periods.map((row) => (
|
||||
<tr>
|
||||
<td>{row.period}</td>
|
||||
<td class="fw-medium">{row.availability}</td>
|
||||
<td class="text-secondary">{row.downtime}</td>
|
||||
<td class="text-secondary">{row.incidents}</td>
|
||||
<td class="text-secondary">{row.longest}</td>
|
||||
<td class="text-secondary">{row.average}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,10 @@ import ProjectSummary from '@shared/components/cards/ProjectSummary.astro'
|
||||
import ProjectKanban from '@shared/components/cards/ProjectKanban.astro'
|
||||
import UserInfo from '@shared/components/cards/UserInfo.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="col-12 col-md-6 col-xxl-3 mb-4 mb-xxl-0">
|
||||
<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 />
|
||||
</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">
|
||||
<ProjectProgress />
|
||||
</div>
|
||||
|
||||
@@ -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'
|
||||
|
||||
const code = `
|
||||
.card-footer {
|
||||
background: transparent;
|
||||
const code = `.card-footer {
|
||||
background: transparent;
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 1 2;
|
||||
}
|
||||
}
|
||||
`
|
||||
&:last-child {
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
}`
|
||||
---
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<CardTitle>Card with code</CardTitle>
|
||||
</div>
|
||||
<div class="card-code">{code}</div>
|
||||
<div class="card-code">
|
||||
<pre><code>{code}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -6,7 +6,7 @@ import PageTitle from '@ui/PageTitle.astro'
|
||||
|
||||
<!-- BEGIN HEADER UPTIME -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container">
|
||||
<div class="container-xl">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="status-indicator status-green status-indicator-animated">
|
||||
|
||||
Reference in new issue
Block a user