Compact dashboard cards with sparklines and recent check data
This commit is contained in:
1 parent
9d2dd8e0c1
commit
e54d6dea5e
13 files changed
+126
-105
No files matched your search
+42
-13
@@ -22,18 +22,25 @@
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.resource-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.1rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
border: 1px solid #f1f1f3;
|
||||
}
|
||||
|
||||
.resource-card.compact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.resource-card:hover {
|
||||
@@ -45,25 +52,25 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.resource-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.2rem 0;
|
||||
}
|
||||
|
||||
.resource-url {
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -95,12 +102,16 @@
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat-value.small {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
@@ -108,6 +119,24 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.resource-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sparkline {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.last-check {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
|
||||
+61
-78
@@ -71,6 +71,65 @@ function Dashboard() {
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const renderResourceCard = (resource) => {
|
||||
const sparkData = (resource.recentChecks || []).map((c, idx) => ({
|
||||
idx,
|
||||
responseTime: c.response_time || 0,
|
||||
statusValue: c.status === 'up' ? 1 : 0,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
key={resource.id}
|
||||
className="resource-card compact"
|
||||
onClick={() => navigate(`/resource/${resource.id}`)}
|
||||
>
|
||||
<div className="resource-header">
|
||||
<div>
|
||||
<h3 className="resource-name">{resource.name}</h3>
|
||||
<p className="resource-url">{resource.url}</p>
|
||||
<p className="resource-type">Type: {resource.type}</p>
|
||||
</div>
|
||||
<span className={`status-badge status-${resource.status}`}>
|
||||
{resource.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{resource.hasActiveIncident && (
|
||||
<div className="incident-badge">⚠️ Active Incident</div>
|
||||
)}
|
||||
|
||||
<div className="resource-meta">
|
||||
<div>
|
||||
<p className="stat-value small">{resource.uptime}%</p>
|
||||
<p className="stat-label">Uptime (24h)</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="stat-value small">{resource.avgResponseTime}ms</p>
|
||||
<p className="stat-label">Avg Resp</p>
|
||||
</div>
|
||||
<div className="sparkline">
|
||||
{sparkData.length > 1 ? (
|
||||
<ResponsiveContainer width="100%" height={50}>
|
||||
<LineChart data={sparkData} margin={{ top: 4, bottom: 0, left: 0, right: 0 }}>
|
||||
<Line type="monotone" dataKey="responseTime" stroke="#667eea" strokeWidth={2} dot={false} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<span style={{ fontSize: '0.75rem', color: '#999' }}>No recent data</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{resource.lastCheck && (
|
||||
<p className="last-check">
|
||||
Last: {new Date(resource.lastCheck).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||||
@@ -98,45 +157,7 @@ function Dashboard() {
|
||||
{group.name}
|
||||
</h3>
|
||||
<div className="dashboard-grid">
|
||||
{groupResourcesMap[group.id]?.map((resource) => (
|
||||
<div
|
||||
key={resource.id}
|
||||
className="resource-card"
|
||||
onClick={() => navigate(`/resource/${resource.id}`)}
|
||||
>
|
||||
<div className="resource-header">
|
||||
<div>
|
||||
<h3 className="resource-name">{resource.name}</h3>
|
||||
<p className="resource-url">{resource.url}</p>
|
||||
<p className="resource-type">Type: {resource.type}</p>
|
||||
</div>
|
||||
<span className={`status-badge status-${resource.status}`}>
|
||||
{resource.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{resource.hasActiveIncident && (
|
||||
<div className="incident-badge">⚠️ Active Incident</div>
|
||||
)}
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat">
|
||||
<p className="stat-value">{resource.uptime}%</p>
|
||||
<p className="stat-label">Uptime (24h)</p>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<p className="stat-value">{resource.avgResponseTime}ms</p>
|
||||
<p className="stat-label">Avg Response</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{resource.lastCheck && (
|
||||
<p style={{ marginTop: '1rem', fontSize: '0.75rem', color: '#999' }}>
|
||||
Last checked: {new Date(resource.lastCheck).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{groupResourcesMap[group.id]?.map(renderResourceCard)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -146,45 +167,7 @@ function Dashboard() {
|
||||
Ungrouped
|
||||
</h3>
|
||||
<div className="dashboard-grid">
|
||||
{groupResourcesMap['ungrouped'].map((resource) => (
|
||||
<div
|
||||
key={resource.id}
|
||||
className="resource-card"
|
||||
onClick={() => navigate(`/resource/${resource.id}`)}
|
||||
>
|
||||
<div className="resource-header">
|
||||
<div>
|
||||
<h3 className="resource-name">{resource.name}</h3>
|
||||
<p className="resource-url">{resource.url}</p>
|
||||
<p className="resource-type">Type: {resource.type}</p>
|
||||
</div>
|
||||
<span className={`status-badge status-${resource.status}`}>
|
||||
{resource.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{resource.hasActiveIncident && (
|
||||
<div className="incident-badge">⚠️ Active Incident</div>
|
||||
)}
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat">
|
||||
<p className="stat-value">{resource.uptime}%</p>
|
||||
<p className="stat-label">Uptime (24h)</p>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<p className="stat-value">{resource.avgResponseTime}ms</p>
|
||||
<p className="stat-label">Avg Response</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{resource.lastCheck && (
|
||||
<p style={{ marginTop: '1rem', fontSize: '0.75rem', color: '#999' }}>
|
||||
Last checked: {new Date(resource.lastCheck).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{groupResourcesMap['ungrouped'].map(renderResourceCard)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user