Add easy & DBA mode
This commit is contained in:
@@ -16,7 +16,7 @@ function Protected({ children }) {
|
||||
}
|
||||
|
||||
function Layout({ children }) {
|
||||
const { me, logout } = useAuth();
|
||||
const { me, logout, uiMode, setUiMode } = useAuth();
|
||||
const navClass = ({ isActive }) => `nav-btn${isActive ? " active" : ""}`;
|
||||
|
||||
return (
|
||||
@@ -63,6 +63,18 @@ function Layout({ children }) {
|
||||
)}
|
||||
</nav>
|
||||
<div className="profile">
|
||||
<div className="mode-switch-block">
|
||||
<div className="mode-switch-label">View Mode</div>
|
||||
<button
|
||||
className={`mode-toggle ${uiMode === "easy" ? "easy" : "dba"}`}
|
||||
onClick={() => setUiMode(uiMode === "easy" ? "dba" : "easy")}
|
||||
type="button"
|
||||
>
|
||||
<span className="mode-pill">Easy</span>
|
||||
<span className="mode-pill">DBA</span>
|
||||
</button>
|
||||
<small>{uiMode === "easy" ? "Simple health guidance" : "Advanced DBA metrics"}</small>
|
||||
</div>
|
||||
<div>{me?.email}</div>
|
||||
<div className="role">{me?.role}</div>
|
||||
<button className="logout-btn" onClick={logout}>Logout</button>
|
||||
|
||||
@@ -66,7 +66,7 @@ async function loadMetric(targetId, metric, range, tokens, refresh) {
|
||||
|
||||
export function TargetDetailPage() {
|
||||
const { id } = useParams();
|
||||
const { tokens, refresh } = useAuth();
|
||||
const { tokens, refresh, uiMode } = useAuth();
|
||||
const [range, setRange] = useState("1h");
|
||||
const [series, setSeries] = useState({});
|
||||
const [locks, setLocks] = useState([]);
|
||||
@@ -135,6 +135,38 @@ export function TargetDetailPage() {
|
||||
[series]
|
||||
);
|
||||
|
||||
const easySummary = useMemo(() => {
|
||||
if (!overview) return null;
|
||||
const latest = chartData[chartData.length - 1];
|
||||
const issues = [];
|
||||
const warnings = [];
|
||||
|
||||
if (overview.partial_failures?.length > 0) warnings.push("Some advanced metrics are currently unavailable.");
|
||||
if ((overview.performance?.deadlocks || 0) > 0) issues.push("Deadlocks were detected.");
|
||||
if ((overview.replication?.mode === "standby") && (overview.replication?.replay_lag_seconds || 0) > 5) {
|
||||
issues.push("Replication lag is above 5 seconds.");
|
||||
}
|
||||
if ((latest?.cache || 0) > 0 && latest.cache < 90) warnings.push("Cache hit ratio is below 90%.");
|
||||
if ((latest?.connections || 0) > 120) warnings.push("Connection count is relatively high.");
|
||||
if ((locks?.length || 0) > 150) warnings.push("High number of active locks.");
|
||||
|
||||
const health = issues.length > 0 ? "problem" : warnings.length > 0 ? "warning" : "ok";
|
||||
const message =
|
||||
health === "ok"
|
||||
? "Everything looks healthy. No major risks detected right now."
|
||||
: health === "warning"
|
||||
? "System is operational, but there are signals you should watch."
|
||||
: "Attention required. Critical signals need investigation.";
|
||||
|
||||
return {
|
||||
health,
|
||||
message,
|
||||
issues,
|
||||
warnings,
|
||||
latest,
|
||||
};
|
||||
}, [overview, chartData, locks]);
|
||||
|
||||
if (loading) return <div className="card">Loading target detail...</div>;
|
||||
if (error) return <div className="card error">{error}</div>;
|
||||
|
||||
@@ -148,7 +180,82 @@ export function TargetDetailPage() {
|
||||
Target Detail {targetMeta?.name || `#${id}`}
|
||||
{targetMeta?.dbname ? ` (${targetMeta.dbname})` : ""}
|
||||
</h2>
|
||||
{overview && (
|
||||
{uiMode === "easy" && overview && easySummary && (
|
||||
<>
|
||||
<div className={`card easy-status ${easySummary.health}`}>
|
||||
<h3>System Health</h3>
|
||||
<p>{easySummary.message}</p>
|
||||
<div className="easy-badge-row">
|
||||
<span className={`easy-badge ${easySummary.health}`}>
|
||||
{easySummary.health === "ok" ? "OK" : easySummary.health === "warning" ? "Warning" : "Problem"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid three">
|
||||
<div className="card stat">
|
||||
<strong>{formatNumber(easySummary.latest?.connections, 0)}</strong>
|
||||
<span>Current Connections</span>
|
||||
</div>
|
||||
<div className="card stat">
|
||||
<strong>{formatNumber(easySummary.latest?.tps, 2)}</strong>
|
||||
<span>Transactions/sec (approx)</span>
|
||||
</div>
|
||||
<div className="card stat">
|
||||
<strong>{formatNumber(easySummary.latest?.cache, 2)}%</strong>
|
||||
<span>Cache Hit Ratio</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h3>Quick Explanation</h3>
|
||||
{easySummary.issues.length === 0 && easySummary.warnings.length === 0 && (
|
||||
<p>No immediate problems were detected. Keep monitoring over time.</p>
|
||||
)}
|
||||
{easySummary.issues.length > 0 && (
|
||||
<>
|
||||
<strong>Problems</strong>
|
||||
<ul className="easy-list">
|
||||
{easySummary.issues.map((item, idx) => <li key={`i-${idx}`}>{item}</li>)}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
{easySummary.warnings.length > 0 && (
|
||||
<>
|
||||
<strong>Things to watch</strong>
|
||||
<ul className="easy-list">
|
||||
{easySummary.warnings.map((item, idx) => <li key={`w-${idx}`}>{item}</li>)}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid two">
|
||||
<div className="card">
|
||||
<h3>Instance Summary</h3>
|
||||
<div className="overview-metrics">
|
||||
<div><span>Role</span><strong>{overview.instance.role}</strong></div>
|
||||
<div><span>Uptime</span><strong>{formatSeconds(overview.instance.uptime_seconds)}</strong></div>
|
||||
<div><span>Target Port</span><strong>{targetMeta?.port ?? "-"}</strong></div>
|
||||
<div><span>Current DB Size</span><strong>{formatBytes(overview.storage.current_database_size_bytes)}</strong></div>
|
||||
<div><span>Replication Clients</span><strong>{overview.replication.active_replication_clients ?? "-"}</strong></div>
|
||||
<div><span>Autovacuum Workers</span><strong>{overview.performance.autovacuum_workers ?? "-"}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h3>Activity Snapshot</h3>
|
||||
<div className="overview-metrics">
|
||||
<div><span>Running Sessions</span><strong>{activity.filter((a) => a.state === "active").length}</strong></div>
|
||||
<div><span>Total Sessions</span><strong>{activity.length}</strong></div>
|
||||
<div><span>Current Locks</span><strong>{locks.length}</strong></div>
|
||||
<div><span>Deadlocks</span><strong>{overview.performance.deadlocks ?? 0}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{uiMode === "dba" && overview && (
|
||||
<div className="card">
|
||||
<h3>Database Overview</h3>
|
||||
<div className="grid three overview-kv">
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useMemo, useState } from "react";
|
||||
import { API_URL } from "./api";
|
||||
|
||||
const AuthCtx = createContext(null);
|
||||
const UI_MODE_KEY = "nexapg_ui_mode";
|
||||
|
||||
function loadStorage() {
|
||||
try {
|
||||
@@ -11,10 +12,21 @@ function loadStorage() {
|
||||
}
|
||||
}
|
||||
|
||||
function loadUiMode() {
|
||||
try {
|
||||
const value = localStorage.getItem(UI_MODE_KEY);
|
||||
if (value === "easy" || value === "dba") return value;
|
||||
} catch {
|
||||
// ignore storage errors
|
||||
}
|
||||
return "dba";
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const initial = loadStorage();
|
||||
const [tokens, setTokens] = useState(initial?.tokens || null);
|
||||
const [me, setMe] = useState(initial?.me || null);
|
||||
const [uiMode, setUiModeState] = useState(loadUiMode);
|
||||
|
||||
const persist = (nextTokens, nextMe) => {
|
||||
if (nextTokens && nextMe) {
|
||||
@@ -78,7 +90,13 @@ export function AuthProvider({ children }) {
|
||||
}
|
||||
};
|
||||
|
||||
const value = useMemo(() => ({ tokens, me, login, logout, refresh }), [tokens, me]);
|
||||
const setUiMode = (nextMode) => {
|
||||
const mode = nextMode === "easy" ? "easy" : "dba";
|
||||
setUiModeState(mode);
|
||||
localStorage.setItem(UI_MODE_KEY, mode);
|
||||
};
|
||||
|
||||
const value = useMemo(() => ({ tokens, me, login, logout, refresh, uiMode, setUiMode }), [tokens, me, uiMode]);
|
||||
return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,52 @@ a {
|
||||
color: #d7e4fa;
|
||||
}
|
||||
|
||||
.mode-switch-block {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px;
|
||||
border: 1px solid #2a588d;
|
||||
border-radius: 10px;
|
||||
background: #0d2244;
|
||||
}
|
||||
|
||||
.mode-switch-label {
|
||||
font-size: 12px;
|
||||
color: #9eb8d6;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.mode-toggle {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 4px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #3170ae;
|
||||
background: #0b1a36;
|
||||
}
|
||||
|
||||
.mode-pill {
|
||||
padding: 7px 6px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #9db4d1;
|
||||
}
|
||||
|
||||
.mode-toggle.easy .mode-pill:first-child,
|
||||
.mode-toggle.dba .mode-pill:last-child {
|
||||
background: linear-gradient(180deg, #165a96, #124978);
|
||||
color: #eff8ff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mode-switch-block small {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #9bb5d4;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.role {
|
||||
color: var(--muted);
|
||||
margin-bottom: 10px;
|
||||
@@ -495,6 +541,57 @@ select:-webkit-autofill {
|
||||
margin: 8px 0 0 18px;
|
||||
}
|
||||
|
||||
.easy-status {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.easy-status.ok {
|
||||
border-color: #1d8d5c;
|
||||
background: linear-gradient(180deg, #123b39, #112a31);
|
||||
}
|
||||
|
||||
.easy-status.warning {
|
||||
border-color: #ad7f25;
|
||||
background: linear-gradient(180deg, #3a3214, #2d2610);
|
||||
}
|
||||
|
||||
.easy-status.problem {
|
||||
border-color: #b54242;
|
||||
background: linear-gradient(180deg, #3f1d22, #2f1519);
|
||||
}
|
||||
|
||||
.easy-badge-row {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.easy-badge {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.easy-badge.ok {
|
||||
color: #86efac;
|
||||
border-color: #2f8d5d;
|
||||
}
|
||||
|
||||
.easy-badge.warning {
|
||||
color: #fde68a;
|
||||
border-color: #9a7a2e;
|
||||
}
|
||||
|
||||
.easy-badge.problem {
|
||||
color: #fecaca;
|
||||
border-color: #b64a4a;
|
||||
}
|
||||
|
||||
.easy-list {
|
||||
margin: 8px 0 10px 16px;
|
||||
}
|
||||
|
||||
.chart-tooltip {
|
||||
background: #0f1934ee;
|
||||
border: 1px solid #2f4a8b;
|
||||
|
||||
Reference in New Issue
Block a user