mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-19 11:45:48 -07:00
Standardized Page Title, Subtitle, and Icon Across All Borealis Pages
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>{' '}
|
||||
❯ <b>Personal Access Tokens ❯ Tokens (Classic) ❯ Generate New Token ❯ 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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user