Standardized Page Title, Subtitle, and Icon Across All Borealis Pages

This commit is contained in:
2025-11-26 06:36:03 -07:00
parent 6634ecc15f
commit d05877a08f
19 changed files with 1060 additions and 621 deletions

View File

@@ -18,6 +18,55 @@ import {
} from "@mui/material";
import UploadIcon from "@mui/icons-material/UploadFile";
import ClearIcon from "@mui/icons-material/Clear";
import VpnKeyIcon from "@mui/icons-material/VpnKey";
const MAGIC_UI = {
panelBg: "rgba(8,12,24,0.96)",
panelBorder: "rgba(148,163,184,0.35)",
textBright: "#e2e8f0",
textMuted: "#94a3b8",
accentA: "#7dd3fc",
accentB: "#c084fc",
danger: "#ff8a8a",
};
const GRADIENT_BUTTON_SX = {
backgroundImage: "linear-gradient(135deg,#7dd3fc,#c084fc)",
color: "#041224",
borderRadius: 999,
textTransform: "none",
fontWeight: 700,
boxShadow: "0 12px 30px rgba(124,58,237,0.32)",
"&:hover": {
backgroundImage: "linear-gradient(135deg,#86e1ff,#d1a6ff)",
boxShadow: "0 14px 38px rgba(124,58,237,0.42)",
},
};
const OUTLINE_BUTTON_SX = {
textTransform: "none",
borderRadius: 2,
borderColor: MAGIC_UI.panelBorder,
color: MAGIC_UI.textBright,
"&:hover": { borderColor: MAGIC_UI.accentA },
};
const INPUT_SX = {
"& .MuiOutlinedInput-root": {
backgroundColor: "rgba(12,18,35,0.75)",
color: MAGIC_UI.textBright,
borderRadius: 2,
"& fieldset": { borderColor: MAGIC_UI.panelBorder },
"&:hover fieldset": { borderColor: MAGIC_UI.accentA },
"&.Mui-focused fieldset": { borderColor: MAGIC_UI.accentA },
},
"& .MuiOutlinedInput-input": {
padding: "10px 12px",
},
"& .MuiInputLabel-root": {
color: MAGIC_UI.textMuted,
},
};
const CREDENTIAL_TYPES = [
{ value: "machine", label: "Machine" },
@@ -280,7 +329,7 @@ export default function CredentialEditor({
};
const title = isEdit ? "Edit Credential" : "Create Credential";
const helperStyle = { fontSize: 12, color: "#8a8a8a", mt: 0.5 };
const helperStyle = { fontSize: 12, color: MAGIC_UI.textMuted, mt: 0.5 };
return (
<Dialog
@@ -288,19 +337,40 @@ export default function CredentialEditor({
onClose={handleCancel}
maxWidth="md"
fullWidth
PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}
PaperProps={{
sx: {
bgcolor: MAGIC_UI.panelBg,
color: MAGIC_UI.textBright,
border: `1px solid ${MAGIC_UI.panelBorder}`,
boxShadow: "0 22px 60px rgba(2,6,23,0.75)",
backdropFilter: "blur(14px)",
},
}}
>
<DialogTitle sx={{ pb: 1 }}>{title}</DialogTitle>
<DialogContent dividers sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<DialogTitle sx={{ pb: 1, display: "flex", alignItems: "center", gap: 1.2 }}>
<VpnKeyIcon sx={{ color: MAGIC_UI.accentA }} />
{title}
</DialogTitle>
<DialogContent
dividers
sx={{ display: "flex", flexDirection: "column", gap: 2, borderColor: MAGIC_UI.panelBorder }}
>
{fetchingDetail && (
<Box sx={{ display: "flex", alignItems: "center", gap: 1, color: "#aaa" }}>
<CircularProgress size={18} sx={{ color: "#58a6ff" }} />
<Box sx={{ display: "flex", alignItems: "center", gap: 1, color: MAGIC_UI.textMuted }}>
<CircularProgress size={18} sx={{ color: MAGIC_UI.accentA }} />
<Typography variant="body2">Loading credential details</Typography>
</Box>
)}
{error && (
<Box sx={{ bgcolor: "#2c1c1c", border: "1px solid #663939", borderRadius: 1, p: 1 }}>
<Typography variant="body2" sx={{ color: "#ff8080" }}>{error}</Typography>
<Box
sx={{
bgcolor: "rgba(255,124,124,0.1)",
border: `1px solid ${MAGIC_UI.panelBorder}`,
borderRadius: 2,
p: 1.5,
}}
>
<Typography variant="body2" sx={{ color: MAGIC_UI.danger }}>{error}</Typography>
</Box>
)}
<TextField
@@ -309,10 +379,7 @@ export default function CredentialEditor({
onChange={updateField("name")}
required
disabled={disableSave}
sx={{
"& .MuiInputBase-root": { bgcolor: "#1f1f1f", color: "#fff" },
"& label": { color: "#888" }
}}
sx={INPUT_SX}
/>
<TextField
label="Description"
@@ -321,19 +388,16 @@ export default function CredentialEditor({
disabled={disableSave}
multiline
minRows={2}
sx={{
"& .MuiInputBase-root": { bgcolor: "#1f1f1f", color: "#fff" },
"& label": { color: "#888" }
}}
sx={INPUT_SX}
/>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2 }}>
<FormControl sx={{ minWidth: 220 }} size="small" disabled={disableSave}>
<InputLabel sx={{ color: "#aaa" }}>Site</InputLabel>
<InputLabel sx={{ color: MAGIC_UI.textMuted }}>Site</InputLabel>
<Select
value={form.site_id}
label="Site"
onChange={updateField("site_id")}
sx={{ bgcolor: "#1f1f1f", color: "#fff" }}
sx={INPUT_SX}
>
<MenuItem value="">(None)</MenuItem>
{sites.map((site) => (
@@ -344,12 +408,12 @@ export default function CredentialEditor({
</Select>
</FormControl>
<FormControl sx={{ minWidth: 180 }} size="small" disabled={disableSave}>
<InputLabel sx={{ color: "#aaa" }}>Credential Type</InputLabel>
<InputLabel sx={{ color: MAGIC_UI.textMuted }}>Credential Type</InputLabel>
<Select
value={form.credential_type}
label="Credential Type"
onChange={updateField("credential_type")}
sx={{ bgcolor: "#1f1f1f", color: "#fff" }}
sx={INPUT_SX}
>
{CREDENTIAL_TYPES.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>{opt.label}</MenuItem>
@@ -357,12 +421,12 @@ export default function CredentialEditor({
</Select>
</FormControl>
<FormControl sx={{ minWidth: 180 }} size="small" disabled={disableSave}>
<InputLabel sx={{ color: "#aaa" }}>Connection</InputLabel>
<InputLabel sx={{ color: MAGIC_UI.textMuted }}>Connection</InputLabel>
<Select
value={form.connection_type}
label="Connection"
onChange={updateField("connection_type")}
sx={{ bgcolor: "#1f1f1f", color: "#fff" }}
sx={INPUT_SX}
>
{CONNECTION_TYPES.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>{opt.label}</MenuItem>
@@ -375,10 +439,7 @@ export default function CredentialEditor({
value={form.username}
onChange={updateField("username")}
disabled={disableSave}
sx={{
"& .MuiInputBase-root": { bgcolor: "#1f1f1f", color: "#fff" },
"& label": { color: "#888" }
}}
sx={INPUT_SX}
/>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<TextField
@@ -387,15 +448,11 @@ export default function CredentialEditor({
value={form.password}
onChange={updateField("password")}
disabled={disableSave}
sx={{
flex: 1,
"& .MuiInputBase-root": { bgcolor: "#1f1f1f", color: "#fff" },
"& label": { color: "#888" }
}}
sx={{ flex: 1, ...INPUT_SX }}
/>
{isEdit && currentCredentialFlags.hasPassword && !passwordDirty && !clearPassword && (
<Tooltip title="Clear stored password">
<IconButton size="small" onClick={() => setClearPassword(true)} sx={{ color: "#ff8080" }}>
<IconButton size="small" onClick={() => setClearPassword(true)} sx={{ color: MAGIC_UI.danger }}>
<ClearIcon fontSize="small" />
</IconButton>
</Tooltip>
@@ -405,7 +462,7 @@ export default function CredentialEditor({
<Typography sx={helperStyle}>Stored password will remain unless you change or clear it.</Typography>
)}
{clearPassword && (
<Typography sx={{ ...helperStyle, color: "#ffaaaa" }}>Password will be removed when saving.</Typography>
<Typography sx={{ ...helperStyle, color: MAGIC_UI.danger }}>Password will be removed when saving.</Typography>
)}
<Box sx={{ display: "flex", gap: 1, alignItems: "flex-start" }}>
@@ -419,8 +476,8 @@ export default function CredentialEditor({
maxRows={12}
sx={{
flex: 1,
"& .MuiInputBase-root": { bgcolor: "#1f1f1f", color: "#fff", fontFamily: "monospace" },
"& label": { color: "#888" }
...INPUT_SX,
"& .MuiOutlinedInput-input": { fontFamily: "monospace" },
}}
/>
<Button
@@ -428,14 +485,14 @@ export default function CredentialEditor({
component="label"
startIcon={<UploadIcon />}
disabled={disableSave}
sx={{ alignSelf: "center", borderColor: "#58a6ff", color: "#58a6ff" }}
sx={{ alignSelf: "center", ...OUTLINE_BUTTON_SX }}
>
Upload
<input type="file" hidden accept=".pem,.key,.txt" onChange={handlePrivateKeyUpload} />
</Button>
{isEdit && currentCredentialFlags.hasPrivateKey && !privateKeyDirty && !clearPrivateKey && (
<Tooltip title="Clear stored private key">
<IconButton size="small" onClick={() => setClearPrivateKey(true)} sx={{ color: "#ff8080" }}>
<IconButton size="small" onClick={() => setClearPrivateKey(true)} sx={{ color: MAGIC_UI.danger }}>
<ClearIcon fontSize="small" />
</IconButton>
</Tooltip>
@@ -445,7 +502,7 @@ export default function CredentialEditor({
<Typography sx={helperStyle}>Private key is stored. Upload or paste a new one to replace, or clear it.</Typography>
)}
{clearPrivateKey && (
<Typography sx={{ ...helperStyle, color: "#ffaaaa" }}>Private key will be removed when saving.</Typography>
<Typography sx={{ ...helperStyle, color: MAGIC_UI.danger }}>Private key will be removed when saving.</Typography>
)}
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
@@ -455,15 +512,11 @@ export default function CredentialEditor({
value={form.private_key_passphrase}
onChange={updateField("private_key_passphrase")}
disabled={disableSave}
sx={{
flex: 1,
"& .MuiInputBase-root": { bgcolor: "#1f1f1f", color: "#fff" },
"& label": { color: "#888" }
}}
sx={{ flex: 1, ...INPUT_SX }}
/>
{isEdit && currentCredentialFlags.hasPrivateKeyPassphrase && !passphraseDirty && !clearPassphrase && (
<Tooltip title="Clear stored passphrase">
<IconButton size="small" onClick={() => setClearPassphrase(true)} sx={{ color: "#ff8080" }}>
<IconButton size="small" onClick={() => setClearPassphrase(true)} sx={{ color: MAGIC_UI.danger }}>
<ClearIcon fontSize="small" />
</IconButton>
</Tooltip>
@@ -473,17 +526,17 @@ export default function CredentialEditor({
<Typography sx={helperStyle}>A passphrase is stored for this key.</Typography>
)}
{clearPassphrase && (
<Typography sx={{ ...helperStyle, color: "#ffaaaa" }}>Key passphrase will be removed when saving.</Typography>
<Typography sx={{ ...helperStyle, color: MAGIC_UI.danger }}>Key passphrase will be removed when saving.</Typography>
)}
<Box sx={{ display: "flex", gap: 2, flexWrap: "wrap" }}>
<FormControl sx={{ minWidth: 180 }} size="small" disabled={disableSave}>
<InputLabel sx={{ color: "#aaa" }}>Privilege Escalation</InputLabel>
<InputLabel sx={{ color: MAGIC_UI.textMuted }}>Privilege Escalation</InputLabel>
<Select
value={form.become_method}
label="Privilege Escalation"
onChange={updateField("become_method")}
sx={{ bgcolor: "#1f1f1f", color: "#fff" }}
sx={INPUT_SX}
>
{BECOME_METHODS.map((opt) => (
<MenuItem key={opt.value || "none"} value={opt.value}>{opt.label}</MenuItem>
@@ -495,12 +548,7 @@ export default function CredentialEditor({
value={form.become_username}
onChange={updateField("become_username")}
disabled={disableSave}
sx={{
flex: 1,
minWidth: 200,
"& .MuiInputBase-root": { bgcolor: "#1f1f1f", color: "#fff" },
"& label": { color: "#888" }
}}
sx={{ flex: 1, minWidth: 200, ...INPUT_SX }}
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
@@ -510,15 +558,11 @@ export default function CredentialEditor({
value={form.become_password}
onChange={updateField("become_password")}
disabled={disableSave}
sx={{
flex: 1,
"& .MuiInputBase-root": { bgcolor: "#1f1f1f", color: "#fff" },
"& label": { color: "#888" }
}}
sx={{ flex: 1, ...INPUT_SX }}
/>
{isEdit && currentCredentialFlags.hasBecomePassword && !becomePasswordDirty && !clearBecomePassword && (
<Tooltip title="Clear stored escalation password">
<IconButton size="small" onClick={() => setClearBecomePassword(true)} sx={{ color: "#ff8080" }}>
<IconButton size="small" onClick={() => setClearBecomePassword(true)} sx={{ color: MAGIC_UI.danger }}>
<ClearIcon fontSize="small" />
</IconButton>
</Tooltip>
@@ -528,20 +572,20 @@ export default function CredentialEditor({
<Typography sx={helperStyle}>Escalation password is stored.</Typography>
)}
{clearBecomePassword && (
<Typography sx={{ ...helperStyle, color: "#ffaaaa" }}>Escalation password will be removed when saving.</Typography>
<Typography sx={{ ...helperStyle, color: MAGIC_UI.danger }}>Escalation password will be removed when saving.</Typography>
)}
</DialogContent>
<DialogActions sx={{ px: 3, py: 2 }}>
<Button onClick={handleCancel} sx={{ color: "#58a6ff" }} disabled={loading}>
<Button onClick={handleCancel} sx={OUTLINE_BUTTON_SX} disabled={loading}>
Cancel
</Button>
<Button
onClick={handleSave}
variant="outlined"
sx={{ color: "#58a6ff", borderColor: "#58a6ff" }}
variant="contained"
sx={GRADIENT_BUTTON_SX}
disabled={disableSave}
>
{loading ? <CircularProgress size={18} sx={{ color: "#58a6ff" }} /> : "Save"}
{loading ? <CircularProgress size={18} sx={{ color: "#041224" }} /> : "Save"}
</Button>
</DialogActions>
</Dialog>

View File

@@ -41,6 +41,18 @@ const myTheme = themeQuartz.withParams({
const themeClassName = myTheme.themeName || "ag-theme-quartz";
const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif';
const iconFontFamily = '"Quartz Regular"';
const gradientButtonSx = {
backgroundImage: "linear-gradient(135deg,#7dd3fc,#c084fc)",
color: "#0b1220",
borderRadius: 999,
textTransform: "none",
boxShadow: "0 10px 26px rgba(124,58,237,0.28)",
"&:hover": {
backgroundImage: "linear-gradient(135deg,#86e1ff,#d1a6ff)",
boxShadow: "0 12px 34px rgba(124,58,237,0.38)",
filter: "none",
},
};
function formatTs(ts) {
if (!ts) return "-";
@@ -62,7 +74,7 @@ function connectionIcon(connection) {
return <ComputerIcon fontSize="small" sx={{ mr: 0.6, color: "#58a6ff" }} />;
}
export default function CredentialList({ isAdmin = false }) {
export default function CredentialList({ isAdmin = false, onPageMetaChange }) {
const [rows, setRows] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
@@ -216,6 +228,15 @@ export default function CredentialList({ isAdmin = false }) {
fetchCredentials();
}, [fetchCredentials]);
useEffect(() => {
onPageMetaChange?.({
page_title: "Credentials",
page_subtitle: "Stored credentials for remote automation tasks and Ansible playbook runs.",
page_icon: LockIcon,
});
return () => onPageMetaChange?.(null);
}, [onPageMetaChange]);
const handleCreate = () => {
setEditorMode("create");
setEditingCredential(null);
@@ -291,9 +312,11 @@ export default function CredentialList({ isAdmin = false }) {
<>
<Paper
sx={{
m: 2,
m: 0,
p: 0,
bgcolor: "transparent",
border: "none",
boxShadow: "none",
fontFamily: gridFontFamily,
color: "#f5f7fa",
display: "flex",
@@ -302,31 +325,32 @@ export default function CredentialList({ isAdmin = false }) {
minWidth: 0,
minHeight: 420
}}
elevation={2}
elevation={0}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
p: 2,
borderBottom: "1px solid #2a2a2a"
px: 2,
pt: 1,
pb: 1,
}}
>
<Box>
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0.3 }}>
Credentials
</Typography>
<Typography variant="body2" sx={{ color: "#aaa" }}>
Stored credentials for remote automation tasks and Ansible playbook runs.
</Typography>
</Box>
<Box sx={{ display: "flex", gap: 1 }}>
<Button
variant="outlined"
size="small"
startIcon={<RefreshIcon />}
sx={{ borderColor: "#58a6ff", color: "#58a6ff" }}
sx={{
borderColor: "rgba(148,163,184,0.35)",
color: "#e2e8f0",
textTransform: "none",
borderRadius: 999,
px: 1.7,
minWidth: 86,
"&:hover": { borderColor: "rgba(148,163,184,0.55)" },
}}
onClick={fetchCredentials}
disabled={loading}
>
@@ -336,7 +360,7 @@ export default function CredentialList({ isAdmin = false }) {
variant="contained"
size="small"
startIcon={<AddIcon />}
sx={{ bgcolor: "#58a6ff", color: "#0b0f19" }}
sx={gradientButtonSx}
onClick={handleCreate}
>
New Credential
@@ -352,7 +376,6 @@ export default function CredentialList({ isAdmin = false }) {
color: "#7db7ff",
px: 2,
py: 1.5,
borderBottom: "1px solid #2a2a2a"
}}
>
<CircularProgress size={18} sx={{ color: "#58a6ff" }} />
@@ -360,7 +383,7 @@ export default function CredentialList({ isAdmin = false }) {
</Box>
)}
{error && (
<Box sx={{ px: 2, py: 1.5, color: "#ff8080", borderBottom: "1px solid #2a2a2a" }}>
<Box sx={{ px: 2, py: 1.5, color: "#ff8080" }}>
<Typography variant="body2">{error}</Typography>
</Box>
)}

View File

@@ -13,11 +13,14 @@ import RefreshIcon from "@mui/icons-material/Refresh";
import SaveIcon from "@mui/icons-material/Save";
import VisibilityIcon from "@mui/icons-material/Visibility";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import GitHubIcon from "@mui/icons-material/GitHub";
const paperSx = {
m: 2,
m: 0,
p: 0,
bgcolor: "transparent",
border: "none",
boxShadow: "none",
color: "#f5f7fa",
display: "flex",
flexDirection: "column",
@@ -29,17 +32,31 @@ const paperSx = {
const fieldSx = {
mt: 2,
"& .MuiOutlinedInput-root": {
bgcolor: "#181818",
bgcolor: "rgba(255,255,255,0.04)",
color: "#f5f7fa",
"& fieldset": { borderColor: "#2a2a2a" },
"&:hover fieldset": { borderColor: "#58a6ff" },
"&.Mui-focused fieldset": { borderColor: "#58a6ff" }
borderRadius: 1,
"& fieldset": { borderColor: "rgba(148,163,184,0.35)" },
"&:hover fieldset": { borderColor: "rgba(148,163,184,0.55)" },
"&.Mui-focused fieldset": { borderColor: "#7dd3fc" }
},
"& .MuiInputLabel-root": { color: "#bbb" },
"& .MuiInputLabel-root.Mui-focused": { color: "#7db7ff" }
};
export default function GithubAPIToken({ isAdmin = false }) {
const gradientButtonSx = {
backgroundImage: "linear-gradient(135deg,#7dd3fc,#c084fc)",
color: "#0b1220",
borderRadius: 999,
textTransform: "none",
boxShadow: "0 10px 26px rgba(124,58,237,0.28)",
"&:hover": {
backgroundImage: "linear-gradient(135deg,#86e1ff,#d1a6ff)",
boxShadow: "0 12px 34px rgba(124,58,237,0.38)",
filter: "none",
},
};
export default function GithubAPIToken({ isAdmin = false, onPageMetaChange }) {
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [token, setToken] = useState("");
@@ -145,6 +162,15 @@ export default function GithubAPIToken({ isAdmin = false }) {
setShowToken((prev) => !prev);
}, []);
useEffect(() => {
onPageMetaChange?.({
page_title: "GitHub API Token",
page_subtitle: "Increase GitHub API rate limits for Borealis by storing a personal access token.",
page_icon: GitHubIcon,
});
return () => onPageMetaChange?.(null);
}, [onPageMetaChange]);
if (!isAdmin) {
return (
<Paper sx={{ m: 2, p: 3, bgcolor: "transparent" }}>
@@ -159,23 +185,10 @@ export default function GithubAPIToken({ isAdmin = false }) {
}
return (
<Paper sx={paperSx} elevation={2}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
p: 2,
borderBottom: "1px solid #2a2a2a"
}}
>
<Box>
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0.3 }}>
Github API Token
</Typography>
<Typography variant="body2" sx={{ color: "#aaa" }}>
Using a Github "Personal Access Token" increases the Github API rate limits from 60/hr to 5,000/hr. This is important for production Borealis usage as it likes to hit its unauthenticated API limits sometimes despite my best efforts.
<br></br>Navigate to{' '}
<Paper sx={paperSx} elevation={0}>
<Box sx={{ px: 2, py: 2, display: "flex", flexDirection: "column", gap: 1.5 }}>
<Typography variant="body2" sx={{ color: "#ccc" }}>
Using a GitHub Personal Access Token raises rate limits from 60/hr to 5,000/hr. Generate one at{" "}
<Link
href="https://github.com/settings/tokens"
target="_blank"
@@ -183,32 +196,26 @@ export default function GithubAPIToken({ isAdmin = false }) {
sx={{ color: "#7db7ff" }}
>
https://github.com/settings/tokens
</Link>{' '}
&#10095; <b>Personal Access Tokens &#10095; Tokens (Classic) &#10095; Generate New Token &#10095; New Personal Access Token (Classic)</b>
</Typography>
<br></br>
</Link>{" "}
under <b>Personal Access Tokens Tokens (Classic)</b>.
</Typography>
<Typography variant="body2" sx={{ color: "#ccc" }}>
<Box component="span" sx={{ fontWeight: 600 }}>Note:</Box>{' '}
<Box component="span" sx={{ fontWeight: 600 }}>Note:</Box>{" "}
<Box component="code" sx={{ bgcolor: "#222", px: 0.75, py: 0.25, borderRadius: 1, fontSize: "0.85rem" }}>
Borealis Automation Platform
</Box>
</Typography>
<Typography variant="body2" sx={{ color: "#ccc" }}>
<Box component="span" sx={{ fontWeight: 600 }}>Scope:</Box>{' '}
{" "}
<Box component="span" sx={{ fontWeight: 600, ml: 2 }}>Scope:</Box>{" "}
<Box component="code" sx={{ bgcolor: "#222", px: 0.75, py: 0.25, borderRadius: 1, fontSize: "0.85rem" }}>
public_repo
</Box>
</Typography>
<Typography variant="body2" sx={{ color: "#ccc" }}>
<Box component="span" sx={{ fontWeight: 600 }}>Expiration:</Box>{' '}
{" "}
<Box component="span" sx={{ fontWeight: 600, ml: 2 }}>Expiration:</Box>{" "}
<Box component="code" sx={{ bgcolor: "#222", px: 0.75, py: 0.25, borderRadius: 1, fontSize: "0.85rem" }}>
No Expiration
</Box>
</Typography>
</Box>
</Box>
<Box sx={{ px: 2, py: 2, display: "flex", flexDirection: "column", gap: 1.5 }}>
<TextField
label="Personal Access Token"
value={inputValue}
@@ -246,13 +253,7 @@ export default function GithubAPIToken({ isAdmin = false }) {
onClick={handleSave}
disabled={saving || loading}
startIcon={!saving ? <SaveIcon /> : null}
sx={{
bgcolor: "#58a6ff",
color: "#0b0f19",
minWidth: 88,
mr: 1,
"&:hover": { bgcolor: "#7db7ff" }
}}
sx={gradientButtonSx}
>
{saving ? <CircularProgress size={16} sx={{ color: "#0b0f19" }} /> : "Save"}
</Button>
@@ -273,7 +274,15 @@ export default function GithubAPIToken({ isAdmin = false }) {
variant="outlined"
size="small"
startIcon={<RefreshIcon />}
sx={{ borderColor: "#58a6ff", color: "#58a6ff" }}
sx={{
borderColor: "rgba(148,163,184,0.35)",
color: "#e2e8f0",
textTransform: "none",
borderRadius: 999,
px: 1.7,
minWidth: 86,
"&:hover": { borderColor: "rgba(148,163,184,0.55)" },
}}
onClick={hydrate}
disabled={loading || saving}
>

View File

@@ -27,10 +27,11 @@ import {
} from "@mui/material";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import FilterListIcon from "@mui/icons-material/FilterList";
import GroupIcon from "@mui/icons-material/Group";
import { ConfirmDeleteDialog } from "../Dialogs.jsx";
/* ---------- Formatting helpers to keep this page in lockstep with Device_List ---------- */
const tablePaperSx = { m: 2, p: 0, bgcolor: "transparent" };
const tablePaperSx = { m: 0, p: 0, bgcolor: "transparent", border: "none", boxShadow: "none" };
const tableSx = {
minWidth: 820,
"& th, & td": {
@@ -51,6 +52,19 @@ const filterFieldSx = {
"&:hover fieldset": { borderColor: "#888" }
}
};
const gradientButtonSx = {
backgroundImage: "linear-gradient(135deg,#7dd3fc,#c084fc)",
color: "#0b1220",
borderRadius: 999,
textTransform: "none",
boxShadow: "0 10px 26px rgba(124,58,237,0.28)",
"&:hover": {
backgroundImage: "linear-gradient(135deg,#86e1ff,#d1a6ff)",
boxShadow: "0 12px 34px rgba(124,58,237,0.38)",
filter: "none",
},
};
/* -------------------------------------------------------------------- */
function formatTs(tsSec) {
@@ -69,7 +83,7 @@ async function sha512(text) {
return arr.map((b) => b.toString(16).padStart(2, "0")).join("");
}
export default function UserManagement({ isAdmin = false }) {
export default function UserManagement({ isAdmin = false, onPageMetaChange }) {
const [rows, setRows] = useState([]); // {username, display_name, role, last_login}
const [orderBy, setOrderBy] = useState("username");
const [order, setOrder] = useState("asc");
@@ -91,6 +105,7 @@ export default function UserManagement({ isAdmin = false }) {
const [mfaBusyUser, setMfaBusyUser] = useState(null);
const [resetMfaOpen, setResetMfaOpen] = useState(false);
const [resetMfaTarget, setResetMfaTarget] = useState(null);
const useGlobalHeader = Boolean(onPageMetaChange);
// Columns and filters
const columns = useMemo(() => ([
@@ -140,6 +155,15 @@ export default function UserManagement({ isAdmin = false }) {
fetchUsers();
}, [fetchUsers, isAdmin]);
useEffect(() => {
onPageMetaChange?.({
page_title: "User Management",
page_subtitle: "Manage platform users, roles, MFA, and credentials.",
page_icon: GroupIcon,
});
return () => onPageMetaChange?.(null);
}, [onPageMetaChange]);
const handleSort = (col) => {
if (orderBy === col) setOrder(order === "asc" ? "desc" : "asc");
else { setOrderBy(col); setOrder("asc"); }
@@ -397,21 +421,13 @@ export default function UserManagement({ isAdmin = false }) {
return (
<>
<Paper sx={tablePaperSx} elevation={2}>
<Box sx={{ p: 2, pb: 1, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Box>
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}>
User Management
</Typography>
<Typography variant="body2" sx={{ color: "#aaa" }}>
Manage authorized users of the Borealis Automation Platform.
</Typography>
</Box>
<Paper sx={tablePaperSx} elevation={0}>
<Box sx={{ p: 2, pb: 1, display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 1 }}>
<Button
variant="outlined"
variant="contained"
size="small"
onClick={openCreate}
sx={{ color: "#58a6ff", borderColor: "#58a6ff", textTransform: "none" }}
sx={gradientButtonSx}
>
Create User
</Button>