mirror of
https://github.com/tabler/tabler.git
synced 2026-10-20 14:54:31 +04:00
feat: add project overview, analytics and logs pages
- ProjectHeader tabs link all five project pages - Datagrid forwards rest attributes; failure summary in two columns, skip icon on Deploy step
This commit is contained in:
1 parent
674b8dbd88
commit
da33dcc6cc
7 files changed
+519
-8
No files matched your search
@@ -2,4 +2,4 @@
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Added the deployment failed and project settings preview pages with a shared `ProjectHeader` component.
|
||||
Added project overview, deployment failed, analytics, logs and settings preview pages with a shared `ProjectHeader`.
|
||||
@@ -47,7 +47,7 @@ type 'Promise<number>'.
|
||||
|
||||
Found 2 errors in 1 file.`,
|
||||
},
|
||||
{ status: 'secondary', name: 'Deploy', time: '-', detail: 'Skipped because Compile failed' },
|
||||
{ status: 'secondary', icon: 'player-skip-forward', name: 'Deploy', time: '-', detail: 'Skipped because Compile failed' },
|
||||
]
|
||||
|
||||
const nextSteps = [
|
||||
@@ -151,7 +151,7 @@ const nextSteps = [
|
||||
<Card>
|
||||
<CardHeader title="Failure summary" />
|
||||
<CardBody>
|
||||
<Datagrid>
|
||||
<Datagrid style="--tblr-datagrid-item-width: 10rem">
|
||||
<DatagridItem title="Exit code"><code>2</code></DatagridItem>
|
||||
<DatagridItem title="Failed step">Compile</DatagridItem>
|
||||
<DatagridItem title="Errors">2 TypeScript errors</DatagridItem>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import ProjectHeader from '@shared/components/parts/ProjectHeader.astro'
|
||||
import Chart from '@shared/components/demo/Chart.astro'
|
||||
import Button from '@ui/Button.astro'
|
||||
import ButtonList from '@ui/ButtonList.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardActions from '@ui/CardActions.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import ChartSparkline from '@ui/ChartSparkline.astro'
|
||||
import NavSegmented from '@ui/NavSegmented.astro'
|
||||
import Progress from '@ui/Progress.astro'
|
||||
|
||||
const stats = [
|
||||
{ id: 'requests', label: 'Requests', value: '8.61M', delta: '+8.2%', color: 'success', data: '21,24,22,27,29,26,31' },
|
||||
{ id: 'visitors', label: 'Unique visitors', value: '142K', delta: '+3.1%', color: 'success', data: '12,14,13,15,14,16,17' },
|
||||
{ id: 'errors', label: 'Error rate', value: '0.42%', delta: '+0.09 pp', color: 'danger', data: '3,2,4,3,5,6,7' },
|
||||
{ id: 'latency', label: 'p95 latency', value: '212 ms', delta: '-14 ms', color: 'success', data: '9,8,8,7,7,6,6' },
|
||||
]
|
||||
|
||||
const routes = [
|
||||
{ path: '/v1/events', requests: '3.2M', share: 37, errors: '0.12%', p95: '148 ms' },
|
||||
{ path: '/v1/users/:id', requests: '1.9M', share: 22, errors: '0.31%', p95: '96 ms' },
|
||||
{ path: '/v1/auth/token', requests: '1.1M', share: 13, errors: '1.84%', p95: '402 ms' },
|
||||
{ path: '/v1/webhooks', requests: '840K', share: 10, errors: '0.05%', p95: '221 ms' },
|
||||
{ path: '/v1/search', requests: '610K', share: 7, errors: '0.44%', p95: '618 ms' },
|
||||
]
|
||||
|
||||
const statusCodes = [
|
||||
{ code: '2xx', share: 97.1, color: 'success' },
|
||||
{ code: '3xx', share: 1.6, color: 'secondary' },
|
||||
{ code: '4xx', share: 0.9, color: 'warning' },
|
||||
{ code: '5xx', share: 0.4, color: 'danger' },
|
||||
]
|
||||
---
|
||||
|
||||
<DefaultLayout title="Project analytics" pageHeader={false} pageMenu="extra.project-analytics" description="Project analytics with request and error charts, top routes and status code breakdown." pageLibs={['apexcharts']}>
|
||||
<ProjectHeader name="pulse-api" section="Analytics" badge="Production" badgeColor="success" active="analytics">
|
||||
<ButtonList>
|
||||
<NavSegmented items={['24h', '7d', '30d']} default={1} name="analytics-range" size="sm" />
|
||||
<Button text="Export" icon="download" />
|
||||
</ButtonList>
|
||||
</ProjectHeader>
|
||||
|
||||
<div class="row row-cards">
|
||||
{
|
||||
stats.map((stat) => (
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div class="text-secondary">{stat.label}</div>
|
||||
<span class:list={['small', 'fw-medium', `text-${stat.color}`]}>{stat.delta}</span>
|
||||
</div>
|
||||
<div class="h1 mb-2">{stat.value}</div>
|
||||
<ChartSparkline id={`analytics-${stat.id}`} type="line" color={stat.color === 'danger' ? 'danger' : 'primary'} data={stat.data} size="sm" />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
<div class="col-lg-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Requests</CardTitle>
|
||||
<CardActions>
|
||||
<span class="text-secondary">Last 7 days, per hour</span>
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Chart chartId="visitors" id="analytics-requests-chart" label="Requests per hour, last 7 days" />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Status codes</CardTitle>
|
||||
<CardActions>
|
||||
<span class="text-secondary">Share of responses</span>
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="space-y">
|
||||
{
|
||||
statusCodes.map((status) => (
|
||||
<div>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="fw-medium">{status.code}</span>
|
||||
<span class="text-secondary">{status.share}%</span>
|
||||
</div>
|
||||
<Progress value={status.share} color={status.color} size="sm" />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<Chart chartId="uptime-incidents" id="analytics-errors-chart" label="Errors per day, last 7 days" size="sm" />
|
||||
<div class="text-secondary mt-2">5xx responses per day</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Top routes</CardTitle>
|
||||
<CardActions>
|
||||
<Button text="All routes" size="sm" />
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<div class="table-responsive">
|
||||
<table class="table card-table table-vcenter text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Route</th>
|
||||
<th>Requests</th>
|
||||
<th class="w-25">Share</th>
|
||||
<th>Error rate</th>
|
||||
<th>p95 latency</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
routes.map((route) => (
|
||||
<tr>
|
||||
<td>
|
||||
<code>{route.path}</code>
|
||||
</td>
|
||||
<td>{route.requests}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<Progress value={route.share} size="sm" class="flex-fill" />
|
||||
<span class="text-secondary">{route.share}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class:list={{ 'text-danger': Number.parseFloat(route.errors) > 1 }}>{route.errors}</td>
|
||||
<td class:list={{ 'text-warning': Number.parseInt(route.p95, 10) > 400 }}>{route.p95}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import ProjectHeader from '@shared/components/parts/ProjectHeader.astro'
|
||||
import Badge from '@ui/Badge.astro'
|
||||
import Button from '@ui/Button.astro'
|
||||
import ButtonList from '@ui/ButtonList.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardActions from '@ui/CardActions.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardFooter from '@ui/CardFooter.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import InputIcon from '@ui/form/InputIcon.astro'
|
||||
import NavSegmented from '@ui/NavSegmented.astro'
|
||||
import StatusDot from '@ui/StatusDot.astro'
|
||||
|
||||
type Level = 'info' | 'warn' | 'error'
|
||||
|
||||
const levels: Record<Level, { text: string; color: string }> = {
|
||||
info: { text: 'INFO', color: 'secondary' },
|
||||
warn: { text: 'WARN', color: 'warning' },
|
||||
error: { text: 'ERROR', color: 'danger' },
|
||||
}
|
||||
|
||||
const statusColor = (status: number) => (status >= 500 ? 'danger' : status >= 400 ? 'warning' : 'success')
|
||||
|
||||
const logs: { time: string; level: Level; status: number; method: string; path: string; duration: string; message: string }[] = [
|
||||
{ time: '09:41:52.114', level: 'info', status: 200, method: 'POST', path: '/v1/events', duration: '48 ms', message: 'Accepted 240 events for workspace ws_8f2a' },
|
||||
{ time: '09:41:51.907', level: 'info', status: 200, method: 'GET', path: '/v1/users/u_1042', duration: '12 ms', message: 'Cache hit' },
|
||||
{ time: '09:41:51.640', level: 'warn', status: 429, method: 'POST', path: '/v1/auth/token', duration: '3 ms', message: 'Rate limit exceeded for client c_77e1, retry after 12 s' },
|
||||
{ time: '09:41:50.982', level: 'error', status: 502, method: 'GET', path: '/v1/search', duration: '2,004 ms', message: 'Upstream search cluster timed out after 2,000 ms' },
|
||||
{ time: '09:41:50.311', level: 'info', status: 201, method: 'POST', path: '/v1/webhooks', duration: '96 ms', message: 'Registered webhook wh_5c31 for order.created' },
|
||||
{ time: '09:41:49.870', level: 'info', status: 304, method: 'GET', path: '/v1/users/u_2210', duration: '9 ms', message: 'Not modified' },
|
||||
{ time: '09:41:49.204', level: 'warn', status: 401, method: 'GET', path: '/v1/events', duration: '4 ms', message: 'Expired token for client c_09aa' },
|
||||
{ time: '09:41:48.556', level: 'error', status: 500, method: 'POST', path: '/v1/events', duration: '311 ms', message: 'Redis connection reset by peer, retrying' },
|
||||
{ time: '09:41:48.020', level: 'info', status: 200, method: 'GET', path: '/v1/health', duration: '1 ms', message: 'OK' },
|
||||
{ time: '09:41:47.402', level: 'info', status: 200, method: 'DELETE', path: '/v1/webhooks/wh_11ab', duration: '38 ms', message: 'Webhook removed' },
|
||||
]
|
||||
---
|
||||
|
||||
<DefaultLayout title="Project logs" pageHeader={false} pageMenu="extra.project-logs" description="Runtime logs with a filter toolbar, request status, level badges and live tail.">
|
||||
<ProjectHeader name="pulse-api" section="Logs" badge="Production" badgeColor="success" active="logs">
|
||||
<ButtonList>
|
||||
<Button text="Pause" icon="player-pause" />
|
||||
<Button text="Download" icon="download" />
|
||||
</ButtonList>
|
||||
</ProjectHeader>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Runtime logs</CardTitle>
|
||||
<CardActions>
|
||||
<span class="d-inline-flex align-items-center gap-2 text-secondary">
|
||||
<StatusDot color="success" animated />
|
||||
Live
|
||||
</span>
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-12 col-md">
|
||||
<InputIcon icon="search" placeholder="Search messages, paths or request ids" ariaLabel="Search logs" />
|
||||
</div>
|
||||
<div class="col-6 col-md-auto">
|
||||
<select class="form-select" aria-label="Log level">
|
||||
<option value="all" selected>All levels</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warn">Warn</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-auto">
|
||||
<select class="form-select" aria-label="Environment">
|
||||
<option value="production" selected>Production</option>
|
||||
<option value="preview">Preview</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-auto">
|
||||
<NavSegmented items={['Live', '1h', '24h']} default={0} name="logs-range" />
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
<div class="table-responsive">
|
||||
<table class="table card-table table-vcenter text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Level</th>
|
||||
<th>Status</th>
|
||||
<th>Request</th>
|
||||
<th>Duration</th>
|
||||
<th class="w-100">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
logs.map((log) => (
|
||||
<tr class:list={{ 'bg-danger-lt': log.level === 'error' }}>
|
||||
<td class="text-secondary font-monospace">{log.time}</td>
|
||||
<td>
|
||||
<Badge text={levels[log.level].text} color={levels[log.level].color} light />
|
||||
</td>
|
||||
<td>
|
||||
<span class="d-inline-flex align-items-center gap-2">
|
||||
<StatusDot color={statusColor(log.status)} />
|
||||
{log.status}
|
||||
</span>
|
||||
</td>
|
||||
<td class="font-monospace">
|
||||
<span class="text-secondary">{log.method}</span> {log.path}
|
||||
</td>
|
||||
<td class:list={['text-secondary', { 'text-danger': log.duration.includes(',') }]}>{log.duration}</td>
|
||||
<td class="text-wrap">{log.message}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<CardFooter class="d-flex align-items-center">
|
||||
<span class="text-secondary">Showing the latest 10 of 1,284 lines</span>
|
||||
<Button text="Load older" size="sm" class="ms-auto" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</DefaultLayout>
|
||||
@@ -0,0 +1,218 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import ProjectHeader from '@shared/components/parts/ProjectHeader.astro'
|
||||
import Badge from '@ui/Badge.astro'
|
||||
import Button from '@ui/Button.astro'
|
||||
import ButtonList from '@ui/ButtonList.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardActions from '@ui/CardActions.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import Datagrid from '@ui/Datagrid.astro'
|
||||
import DatagridItem from '@ui/DatagridItem.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import ListGroup from '@ui/ListGroup.astro'
|
||||
import ListGroupItem from '@ui/ListGroupItem.astro'
|
||||
import StatusDot from '@ui/StatusDot.astro'
|
||||
|
||||
const stats = [
|
||||
{ label: 'Requests, 24h', value: '1.24M', hint: '+8% vs. yesterday', color: 'success' },
|
||||
{ label: 'Error rate', value: '0.42%', hint: '5,208 errors', color: 'warning' },
|
||||
{ label: 'p95 latency', value: '212 ms', hint: '-14 ms vs. yesterday', color: 'success' },
|
||||
{ label: 'Uptime, 30d', value: '99.98%', hint: '1 incident, 9 min', color: 'success' },
|
||||
]
|
||||
|
||||
type Deployment = {
|
||||
status: 'success' | 'danger' | 'warning'
|
||||
statusText: string
|
||||
message: string
|
||||
branch: string
|
||||
commit: string
|
||||
environment: 'Production' | 'Preview'
|
||||
time: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
const deployments: Deployment[] = [
|
||||
{ status: 'danger', statusText: 'Error', message: 'Migrate rate limiter to Redis', branch: 'feat/redis-limiter', commit: '2e77b09', environment: 'Preview', time: '1 day ago', href: './deployment-failed.html' },
|
||||
{ status: 'success', statusText: 'Ready', message: 'Add request id to access logs', branch: 'main', commit: 'a91c3f2', environment: 'Production', time: '3 hours ago' },
|
||||
{ status: 'success', statusText: 'Ready', message: 'Bump Node.js to 22.x', branch: 'main', commit: '7d0e4b1', environment: 'Production', time: '2 days ago' },
|
||||
{ status: 'warning', statusText: 'Building', message: 'Cache webhook signatures', branch: 'feat/webhook-cache', commit: 'c58a2d7', environment: 'Preview', time: 'just now' },
|
||||
]
|
||||
|
||||
const domains = [
|
||||
{ host: 'api.pulsehq.dev', status: 'success', text: 'Valid configuration' },
|
||||
{ host: 'pulse-api.relay.app', status: 'success', text: 'Auto-assigned' },
|
||||
{ host: 'staging-api.pulsehq.dev', status: 'warning', text: 'DNS pending' },
|
||||
]
|
||||
|
||||
const activity = [
|
||||
{ icon: 'rocket', color: 'success', text: 'Deployed a91c3f2 to production', time: '3 hours ago' },
|
||||
{ icon: 'alert-triangle', color: 'danger', text: 'Preview build failed on feat/redis-limiter', time: '1 day ago' },
|
||||
{ icon: 'key', color: 'primary', text: 'k.adamski updated REDIS_URL in Production', time: '1 day ago' },
|
||||
{ icon: 'world', color: 'secondary', text: 'Added domain staging-api.pulsehq.dev', time: '2 days ago' },
|
||||
]
|
||||
---
|
||||
|
||||
<DefaultLayout title="Project overview" pageHeader={false} pageMenu="extra.project-overview" description="Project overview with the production deployment, key metrics, latest deployments, domains and activity.">
|
||||
<ProjectHeader name="pulse-api" section="Overview" badge="Production" badgeColor="success" active="overview">
|
||||
<ButtonList>
|
||||
<Button text="Visit" icon="external-link" href="#" />
|
||||
<Button text="Deploy" color="primary" icon="rocket" />
|
||||
</ButtonList>
|
||||
</ProjectHeader>
|
||||
|
||||
<div class="row row-cards">
|
||||
<div class="col-lg-8">
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Production deployment</CardTitle>
|
||||
<CardActions>
|
||||
<Button text="Redeploy" size="sm" icon="refresh" />
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Datagrid>
|
||||
<DatagridItem title="Status">
|
||||
<span class="d-inline-flex align-items-center gap-2">
|
||||
<StatusDot color="success" animated />
|
||||
Ready
|
||||
</span>
|
||||
<span class="text-secondary ms-1">since 3 hours ago</span>
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Source">
|
||||
<a href="#" class="text-reset">
|
||||
<Icon name="git-branch" class="icon-inline" />
|
||||
main
|
||||
</a>
|
||||
<span class="text-secondary ms-1"><code>a91c3f2</code> Add request id to access logs</span>
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Domain">
|
||||
<a href="#">api.pulsehq.dev</a>
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Runtime">Node.js 22.x, fra1</DatagridItem>
|
||||
</Datagrid>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<div class="row g-4">
|
||||
{
|
||||
stats.map((stat) => (
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="text-secondary">{stat.label}</div>
|
||||
<div class="h2 mb-1">{stat.value}</div>
|
||||
<div class:list={['small', `text-${stat.color}`]}>{stat.hint}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Latest deployments</CardTitle>
|
||||
<CardActions>
|
||||
<Button text="View all" size="sm" href="./deployment-failed.html" />
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<ListGroup class="card-list-group" hoverable>
|
||||
{
|
||||
deployments.map((deployment) => (
|
||||
<ListGroupItem>
|
||||
<div class="row align-items-center g-3">
|
||||
<div class="col-auto">
|
||||
<StatusDot color={deployment.status} animated={deployment.status === 'warning'} ariaLabel={deployment.statusText} />
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="fw-medium">
|
||||
{deployment.href ? (
|
||||
<a href={deployment.href} class="text-reset">
|
||||
{deployment.message}
|
||||
</a>
|
||||
) : (
|
||||
deployment.message
|
||||
)}
|
||||
</div>
|
||||
<div class="text-secondary">
|
||||
<Icon name="git-branch" class="icon-inline" />
|
||||
{deployment.branch} <code>{deployment.commit}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<Badge text={deployment.environment} color={deployment.environment === 'Production' ? 'primary' : 'secondary'} light />
|
||||
</div>
|
||||
<div class="col-auto text-secondary">{deployment.time}</div>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Domains</CardTitle>
|
||||
<CardActions>
|
||||
<Button text="Manage" size="sm" href="./project-settings.html" />
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<ListGroup class="card-list-group">
|
||||
{
|
||||
domains.map((domain) => (
|
||||
<ListGroupItem>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<StatusDot color={domain.status} />
|
||||
<div>
|
||||
<div class="fw-medium">{domain.host}</div>
|
||||
<div class="text-secondary">{domain.text}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Activity</CardTitle>
|
||||
<CardActions>
|
||||
<Button text="View logs" size="sm" href="./project-logs.html" />
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<ListGroup class="card-list-group">
|
||||
{
|
||||
activity.map((item) => (
|
||||
<ListGroupItem>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class:list={['avatar', 'avatar-sm', `bg-${item.color}-lt`]}>
|
||||
<Icon name={item.icon} color={item.color} />
|
||||
</span>
|
||||
<div>
|
||||
<div>{item.text}</div>
|
||||
<div class="text-secondary">{item.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@@ -24,11 +24,11 @@ interface Props {
|
||||
|
||||
const { name, section, badge, badgeColor, active } = Astro.props
|
||||
|
||||
const tabs: { key: Tab; label: string; href?: string }[] = [
|
||||
{ key: 'overview', label: 'Overview' },
|
||||
const tabs: { key: Tab; label: string; href: string }[] = [
|
||||
{ key: 'overview', label: 'Overview', href: './project-overview.html' },
|
||||
{ key: 'deployments', label: 'Deployments', href: './deployment-failed.html' },
|
||||
{ key: 'analytics', label: 'Analytics' },
|
||||
{ key: 'logs', label: 'Logs' },
|
||||
{ key: 'analytics', label: 'Analytics', href: './project-analytics.html' },
|
||||
{ key: 'logs', label: 'Logs', href: './project-logs.html' },
|
||||
{ key: 'settings', label: 'Settings', href: './project-settings.html' },
|
||||
]
|
||||
---
|
||||
@@ -53,7 +53,7 @@ const tabs: { key: Tab; label: string; href?: string }[] = [
|
||||
{
|
||||
tabs.map((tab) => (
|
||||
<li class="nav-item">
|
||||
<a class:list={['nav-link', 'text-nowrap', { active: tab.key === active }]} href={tab.href ?? '#'} aria-current={tab.key === active ? 'page' : undefined}>
|
||||
<a class:list={['nav-link', 'text-nowrap', { active: tab.key === active }]} href={tab.href} aria-current={tab.key === active ? 'page' : undefined}>
|
||||
{tab.label}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -408,6 +408,21 @@
|
||||
"url": "pricing-table.html",
|
||||
"title": "Pricing table"
|
||||
},
|
||||
"project-overview": {
|
||||
"title": "Project overview",
|
||||
"url": "project-overview.html",
|
||||
"badge": "New"
|
||||
},
|
||||
"project-analytics": {
|
||||
"title": "Project analytics",
|
||||
"url": "project-analytics.html",
|
||||
"badge": "New"
|
||||
},
|
||||
"project-logs": {
|
||||
"title": "Project logs",
|
||||
"url": "project-logs.html",
|
||||
"badge": "New"
|
||||
},
|
||||
"project-settings": {
|
||||
"title": "Project settings",
|
||||
"url": "project-settings.html",
|
||||
|
||||
Reference in new issue
Block a user