Improve alert handling and UI for Alerts and Dashboard pages
Added expandable alert details to the Alerts page, providing more insights into each warning or alert. Enhanced the Dashboard to display distinct counts for warnings and alerts, along with updated KPIs and improved styling for better visual hierarchy. These changes improve usability and clarity in monitoring alert statuses.
This commit is contained in:
@@ -19,12 +19,18 @@ function formatAlertValue(value) {
|
|||||||
return Number(value).toFixed(2);
|
return Number(value).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTs(ts) {
|
||||||
|
if (!ts) return "-";
|
||||||
|
return new Date(ts).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
export function AlertsPage() {
|
export function AlertsPage() {
|
||||||
const { tokens, refresh, me } = useAuth();
|
const { tokens, refresh, me } = useAuth();
|
||||||
const [targets, setTargets] = useState([]);
|
const [targets, setTargets] = useState([]);
|
||||||
const [status, setStatus] = useState({ warnings: [], alerts: [], warning_count: 0, alert_count: 0 });
|
const [status, setStatus] = useState({ warnings: [], alerts: [], warning_count: 0, alert_count: 0 });
|
||||||
const [definitions, setDefinitions] = useState([]);
|
const [definitions, setDefinitions] = useState([]);
|
||||||
const [form, setForm] = useState(initialForm);
|
const [form, setForm] = useState(initialForm);
|
||||||
|
const [expandedKey, setExpandedKey] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
@@ -160,6 +166,10 @@ export function AlertsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleExpanded = (key) => {
|
||||||
|
setExpandedKey((prev) => (prev === key ? "" : key));
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) return <div className="card">Loading alerts...</div>;
|
if (loading) return <div className="card">Loading alerts...</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -184,8 +194,16 @@ export function AlertsPage() {
|
|||||||
<h3>Warnings</h3>
|
<h3>Warnings</h3>
|
||||||
{status.warnings?.length ? (
|
{status.warnings?.length ? (
|
||||||
<div className="alerts-list">
|
<div className="alerts-list">
|
||||||
{status.warnings.map((item) => (
|
{status.warnings.map((item) => {
|
||||||
<article className="alert-item warning" key={item.alert_key}>
|
const isOpen = expandedKey === item.alert_key;
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={`alert-item warning ${isOpen ? "is-open" : ""}`}
|
||||||
|
key={item.alert_key}
|
||||||
|
onClick={() => toggleExpanded(item.alert_key)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<div className="alert-item-head">
|
<div className="alert-item-head">
|
||||||
<span className="alert-badge warning">Warning</span>
|
<span className="alert-badge warning">Warning</span>
|
||||||
<strong>{item.name}</strong>
|
<strong>{item.name}</strong>
|
||||||
@@ -193,8 +211,22 @@ export function AlertsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<p>{item.description}</p>
|
<p>{item.description}</p>
|
||||||
<p className="alert-message">{item.message}</p>
|
<p className="alert-message">{item.message}</p>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="alert-details-grid">
|
||||||
|
<div><span>Source</span><strong>{item.source}</strong></div>
|
||||||
|
<div><span>Category</span><strong>{item.category}</strong></div>
|
||||||
|
<div><span>Current Value</span><strong>{formatAlertValue(item.value)}</strong></div>
|
||||||
|
<div><span>Comparison</span><strong>{item.comparison}</strong></div>
|
||||||
|
<div><span>Warning Threshold</span><strong>{formatAlertValue(item.warning_threshold)}</strong></div>
|
||||||
|
<div><span>Alert Threshold</span><strong>{formatAlertValue(item.alert_threshold)}</strong></div>
|
||||||
|
<div><span>Checked At</span><strong>{formatTs(item.checked_at)}</strong></div>
|
||||||
|
<div><span>Target ID</span><strong>{item.target_id}</strong></div>
|
||||||
|
{item.sql_text && <div className="alert-sql"><code>{item.sql_text}</code></div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</article>
|
</article>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="muted">No warning-level alerts right now.</p>
|
<p className="muted">No warning-level alerts right now.</p>
|
||||||
@@ -205,8 +237,16 @@ export function AlertsPage() {
|
|||||||
<h3>Alerts</h3>
|
<h3>Alerts</h3>
|
||||||
{status.alerts?.length ? (
|
{status.alerts?.length ? (
|
||||||
<div className="alerts-list">
|
<div className="alerts-list">
|
||||||
{status.alerts.map((item) => (
|
{status.alerts.map((item) => {
|
||||||
<article className="alert-item alert" key={item.alert_key}>
|
const isOpen = expandedKey === item.alert_key;
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={`alert-item alert ${isOpen ? "is-open" : ""}`}
|
||||||
|
key={item.alert_key}
|
||||||
|
onClick={() => toggleExpanded(item.alert_key)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<div className="alert-item-head">
|
<div className="alert-item-head">
|
||||||
<span className="alert-badge alert">Alert</span>
|
<span className="alert-badge alert">Alert</span>
|
||||||
<strong>{item.name}</strong>
|
<strong>{item.name}</strong>
|
||||||
@@ -214,8 +254,22 @@ export function AlertsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<p>{item.description}</p>
|
<p>{item.description}</p>
|
||||||
<p className="alert-message">{item.message}</p>
|
<p className="alert-message">{item.message}</p>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="alert-details-grid">
|
||||||
|
<div><span>Source</span><strong>{item.source}</strong></div>
|
||||||
|
<div><span>Category</span><strong>{item.category}</strong></div>
|
||||||
|
<div><span>Current Value</span><strong>{formatAlertValue(item.value)}</strong></div>
|
||||||
|
<div><span>Comparison</span><strong>{item.comparison}</strong></div>
|
||||||
|
<div><span>Warning Threshold</span><strong>{formatAlertValue(item.warning_threshold)}</strong></div>
|
||||||
|
<div><span>Alert Threshold</span><strong>{formatAlertValue(item.alert_threshold)}</strong></div>
|
||||||
|
<div><span>Checked At</span><strong>{formatTs(item.checked_at)}</strong></div>
|
||||||
|
<div><span>Target ID</span><strong>{item.target_id}</strong></div>
|
||||||
|
{item.sql_text && <div className="alert-sql"><code>{item.sql_text}</code></div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</article>
|
</article>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="muted">No critical alerts right now.</p>
|
<p className="muted">No critical alerts right now.</p>
|
||||||
@@ -225,9 +279,14 @@ export function AlertsPage() {
|
|||||||
|
|
||||||
{canManageAlerts && (
|
{canManageAlerts && (
|
||||||
<>
|
<>
|
||||||
<section className="card">
|
<details className="card collapsible">
|
||||||
|
<summary className="collapse-head">
|
||||||
|
<div>
|
||||||
<h3>Create Custom Alert</h3>
|
<h3>Create Custom Alert</h3>
|
||||||
<p className="muted">Admins and operators can add SQL-based checks with warning and alert thresholds.</p>
|
<p>Admins and operators can add SQL-based checks with warning and alert thresholds.</p>
|
||||||
|
</div>
|
||||||
|
<span className="collapse-chevron" aria-hidden="true">v</span>
|
||||||
|
</summary>
|
||||||
<form className="alert-form grid two" onSubmit={createDefinition}>
|
<form className="alert-form grid two" onSubmit={createDefinition}>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
@@ -246,7 +305,7 @@ export function AlertsPage() {
|
|||||||
<div className="field">
|
<div className="field">
|
||||||
<label>Comparison</label>
|
<label>Comparison</label>
|
||||||
<select value={form.comparison} onChange={(e) => setForm({ ...form, comparison: e.target.value })}>
|
<select value={form.comparison} onChange={(e) => setForm({ ...form, comparison: e.target.value })}>
|
||||||
<option value="gte">greater than or equal (>=)</option>
|
<option value="gte">greater than or equal (>=)</option>
|
||||||
<option value="gt">greater than (>)</option>
|
<option value="gt">greater than (>)</option>
|
||||||
<option value="lte">less than or equal (<=)</option>
|
<option value="lte">less than or equal (<=)</option>
|
||||||
<option value="lt">less than (<)</option>
|
<option value="lt">less than (<)</option>
|
||||||
@@ -284,10 +343,16 @@ export function AlertsPage() {
|
|||||||
</div>
|
</div>
|
||||||
{testResult && <div className="test-connection-result">{testResult}</div>}
|
{testResult && <div className="test-connection-result">{testResult}</div>}
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</details>
|
||||||
|
|
||||||
<section className="card">
|
<details className="card collapsible">
|
||||||
|
<summary className="collapse-head">
|
||||||
|
<div>
|
||||||
<h3>Custom Alert Definitions</h3>
|
<h3>Custom Alert Definitions</h3>
|
||||||
|
<p>All saved SQL-based alert rules.</p>
|
||||||
|
</div>
|
||||||
|
<span className="collapse-chevron" aria-hidden="true">v</span>
|
||||||
|
</summary>
|
||||||
{definitions.length ? (
|
{definitions.length ? (
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -325,7 +390,7 @@ export function AlertsPage() {
|
|||||||
) : (
|
) : (
|
||||||
<p className="muted">No custom alerts created yet.</p>
|
<p className="muted">No custom alerts created yet.</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</details>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useAuth } from "../state";
|
|||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { tokens, refresh } = useAuth();
|
const { tokens, refresh } = useAuth();
|
||||||
const [targets, setTargets] = useState([]);
|
const [targets, setTargets] = useState([]);
|
||||||
|
const [alertStatus, setAlertStatus] = useState({ warnings: [], alerts: [], warning_count: 0, alert_count: 0 });
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@@ -14,8 +15,14 @@ export function DashboardPage() {
|
|||||||
let active = true;
|
let active = true;
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await apiFetch("/targets", {}, tokens, refresh);
|
const [targetRows, alerts] = await Promise.all([
|
||||||
if (active) setTargets(data);
|
apiFetch("/targets", {}, tokens, refresh),
|
||||||
|
apiFetch("/alerts/status", {}, tokens, refresh),
|
||||||
|
]);
|
||||||
|
if (active) {
|
||||||
|
setTargets(targetRows);
|
||||||
|
setAlertStatus(alerts);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (active) setError(String(e.message || e));
|
if (active) setError(String(e.message || e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -30,8 +37,16 @@ export function DashboardPage() {
|
|||||||
if (loading) return <div className="card">Loading dashboard...</div>;
|
if (loading) return <div className="card">Loading dashboard...</div>;
|
||||||
if (error) return <div className="card error">{error}</div>;
|
if (error) return <div className="card error">{error}</div>;
|
||||||
|
|
||||||
const alerts = targets.filter((t) => !t.host || !t.dbname).length;
|
const targetSeverities = new Map();
|
||||||
const okCount = Math.max(0, targets.length - alerts);
|
for (const item of alertStatus.warnings || []) {
|
||||||
|
if (!targetSeverities.has(item.target_id)) targetSeverities.set(item.target_id, "warning");
|
||||||
|
}
|
||||||
|
for (const item of alertStatus.alerts || []) {
|
||||||
|
targetSeverities.set(item.target_id, "alert");
|
||||||
|
}
|
||||||
|
|
||||||
|
const affectedTargetCount = targetSeverities.size;
|
||||||
|
const okCount = Math.max(0, targets.length - affectedTargetCount);
|
||||||
const filteredTargets = targets.filter((t) => {
|
const filteredTargets = targets.filter((t) => {
|
||||||
const q = search.trim().toLowerCase();
|
const q = search.trim().toLowerCase();
|
||||||
if (!q) return true;
|
if (!q) return true;
|
||||||
@@ -46,7 +61,7 @@ export function DashboardPage() {
|
|||||||
<div className="dashboard-page">
|
<div className="dashboard-page">
|
||||||
<h2>Dashboard Overview</h2>
|
<h2>Dashboard Overview</h2>
|
||||||
<p className="dashboard-subtitle">Real-time snapshot of monitored PostgreSQL targets.</p>
|
<p className="dashboard-subtitle">Real-time snapshot of monitored PostgreSQL targets.</p>
|
||||||
<div className="grid three dashboard-kpis">
|
<div className="dashboard-kpis-grid">
|
||||||
<div className="card stat kpi-card">
|
<div className="card stat kpi-card">
|
||||||
<div className="kpi-orb blue" />
|
<div className="kpi-orb blue" />
|
||||||
<strong>{targets.length}</strong>
|
<strong>{targets.length}</strong>
|
||||||
@@ -57,9 +72,14 @@ export function DashboardPage() {
|
|||||||
<strong>{okCount}</strong>
|
<strong>{okCount}</strong>
|
||||||
<span className="kpi-label">Status OK</span>
|
<span className="kpi-label">Status OK</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="card stat kpi-card alert">
|
<div className="card stat kpi-card warning">
|
||||||
<div className="kpi-orb amber" />
|
<div className="kpi-orb amber" />
|
||||||
<strong>{alerts}</strong>
|
<strong>{alertStatus.warning_count || 0}</strong>
|
||||||
|
<span className="kpi-label">Warnings</span>
|
||||||
|
</div>
|
||||||
|
<div className="card stat kpi-card alert">
|
||||||
|
<div className="kpi-orb red" />
|
||||||
|
<strong>{alertStatus.alert_count || 0}</strong>
|
||||||
<span className="kpi-label">Alerts</span>
|
<span className="kpi-label">Alerts</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,21 +101,21 @@ export function DashboardPage() {
|
|||||||
|
|
||||||
<div className="dashboard-target-list">
|
<div className="dashboard-target-list">
|
||||||
{filteredTargets.map((t) => {
|
{filteredTargets.map((t) => {
|
||||||
const hasAlert = !t.host || !t.dbname;
|
const severity = targetSeverities.get(t.id) || "ok";
|
||||||
return (
|
return (
|
||||||
<article className="dashboard-target-card" key={t.id}>
|
<article className="dashboard-target-card" key={t.id}>
|
||||||
<div className="target-main">
|
<div className="target-main">
|
||||||
<div className="target-title-row">
|
<div className="target-title-row">
|
||||||
<h4>{t.name}</h4>
|
<h4>{t.name}</h4>
|
||||||
<span className={`status-chip ${hasAlert ? "alert" : "ok"}`}>
|
<span className={`status-chip ${severity}`}>
|
||||||
{hasAlert ? "Alert" : "OK"}
|
{severity === "alert" ? "Alert" : severity === "warning" ? "Warning" : "OK"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p><strong>Host:</strong> {t.host}:{t.port}</p>
|
<p><strong>Host:</strong> {t.host}:{t.port}</p>
|
||||||
<p><strong>DB:</strong> {t.dbname}</p>
|
<p><strong>DB:</strong> {t.dbname}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="target-actions">
|
<div className="target-actions">
|
||||||
<Link className="details-btn" to={`/targets/${t.id}`}>Details <span aria-hidden="true">→</span></Link>
|
<Link className="details-btn" to={`/targets/${t.id}`}>Details <span aria-hidden="true">{"->"}</span></Link>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -264,14 +264,21 @@ button {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-kpis .kpi-card {
|
.dashboard-kpis-grid .kpi-card {
|
||||||
border-left: 4px solid #2f7eca;
|
border-left: 4px solid #2f7eca;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 12px 30px #0416344d;
|
box-shadow: 0 12px 30px #0416344d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-kpis .kpi-card::after {
|
.dashboard-kpis-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-kpis-grid .kpi-card::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -30px;
|
right: -30px;
|
||||||
@@ -282,12 +289,16 @@ button {
|
|||||||
background: radial-gradient(circle, #5bc2ff33, transparent 70%);
|
background: radial-gradient(circle, #5bc2ff33, transparent 70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-kpis .kpi-card.ok {
|
.dashboard-kpis-grid .kpi-card.ok {
|
||||||
border-left-color: #2fa86f;
|
border-left-color: #2fa86f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-kpis .kpi-card.alert {
|
.dashboard-kpis-grid .kpi-card.warning {
|
||||||
border-left-color: #d47a2a;
|
border-left-color: #f39c1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-kpis-grid .kpi-card.alert {
|
||||||
|
border-left-color: #ff5f6d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-targets-head {
|
.dashboard-targets-head {
|
||||||
@@ -341,6 +352,10 @@ button {
|
|||||||
background: #f2a43a;
|
background: #f2a43a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kpi-orb.red {
|
||||||
|
background: #ff5570;
|
||||||
|
}
|
||||||
|
|
||||||
.kpi-label {
|
.kpi-label {
|
||||||
color: #b9d2ed;
|
color: #b9d2ed;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -412,9 +427,15 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-chip.alert {
|
.status-chip.alert {
|
||||||
color: #fde68a;
|
color: #fecaca;
|
||||||
background: #3a2c12;
|
background: #401724;
|
||||||
border-color: #9b7b2f;
|
border-color: #bd4761;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip.warning {
|
||||||
|
color: #ffe4af;
|
||||||
|
background: #3e2b11;
|
||||||
|
border-color: #c78824;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details-btn {
|
.details-btn {
|
||||||
@@ -789,13 +810,15 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alerts-kpi.warning {
|
.alerts-kpi.warning {
|
||||||
border-color: #9a6426;
|
border-color: #f39c1f;
|
||||||
background: linear-gradient(180deg, #342713, #251b0f);
|
background: linear-gradient(180deg, #4e300f, #32200d);
|
||||||
|
box-shadow: 0 10px 24px #c6771238, inset 0 1px 0 #ffd28030;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-kpi.alert {
|
.alerts-kpi.alert {
|
||||||
border-color: #a53a46;
|
border-color: #ff5f6d;
|
||||||
background: linear-gradient(180deg, #381520, #2b1018);
|
background: linear-gradient(180deg, #5a1627, #3b111c);
|
||||||
|
box-shadow: 0 10px 24px #bf2f4f3f, inset 0 1px 0 #ff9aad2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-list {
|
.alerts-list {
|
||||||
@@ -814,13 +837,21 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-item.warning {
|
.alert-item.warning {
|
||||||
border-color: #8a6d34;
|
border-color: #d38f2a;
|
||||||
background: linear-gradient(180deg, #2f2516, #261f15);
|
background: linear-gradient(180deg, #4a3012, #34220f);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-item.alert {
|
.alert-item.alert {
|
||||||
border-color: #9f3e4a;
|
border-color: #d8526a;
|
||||||
background: linear-gradient(180deg, #361822, #2d131b);
|
background: linear-gradient(180deg, #52202e, #38151f);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-item:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-item.is-open {
|
||||||
|
box-shadow: 0 14px 28px #07163166;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-item-head {
|
.alert-item-head {
|
||||||
@@ -855,15 +886,54 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-badge.warning {
|
.alert-badge.warning {
|
||||||
color: #f9d8a8;
|
color: #ffe8bf;
|
||||||
background: #3a2c16;
|
background: #5d370f;
|
||||||
border-color: #a1742f;
|
border-color: #e79f3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-badge.alert {
|
.alert-badge.alert {
|
||||||
color: #fecaca;
|
color: #ffe0e4;
|
||||||
background: #3a1620;
|
background: #5e1929;
|
||||||
border-color: #ad4552;
|
border-color: #f06a81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-details-grid {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #ffffff20;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-details-grid div {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-details-grid span {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #b7cbe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-details-grid strong {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #edf4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-sql {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #081a33a6;
|
||||||
|
border: 1px solid #376097;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-sql code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-form .field-full {
|
.alert-form .field-full {
|
||||||
@@ -1161,6 +1231,9 @@ select:-webkit-autofill {
|
|||||||
.grid.three {
|
.grid.three {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
.dashboard-kpis-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
.main {
|
.main {
|
||||||
height: auto;
|
height: auto;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|||||||
Reference in New Issue
Block a user