Standardized Position of Page-Level Action Buttons Across All of Borealis

This commit is contained in:
2025-11-28 20:39:23 -07:00
parent 6ca42ec3a4
commit c1d6f4edda
8 changed files with 404 additions and 213 deletions

View File

@@ -8,6 +8,7 @@ import {
Paper,
Typography,
CircularProgress,
Stack,
} from "@mui/material";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import AddIcon from "@mui/icons-material/Add";
@@ -310,6 +311,53 @@ export default function CredentialList({ isAdmin = false, onPageMetaChange }) {
return (
<>
<Box
sx={{
position: "fixed",
top: { xs: 72, md: 88 },
right: { xs: 12, md: 20 },
zIndex: 1400,
pointerEvents: "none",
}}
>
<Stack direction="row" spacing={1.25} sx={{ pointerEvents: "auto" }}>
<Button
variant="outlined"
size="small"
startIcon={<RefreshIcon />}
sx={{
borderColor: "rgba(148,163,184,0.4)",
color: "#e2e8f0",
textTransform: "none",
borderRadius: 999,
px: 1.9,
minWidth: 112,
backgroundColor: "rgba(12,18,35,0.85)",
"&:hover": {
borderColor: "rgba(125,211,252,0.85)",
backgroundColor: "rgba(16,24,44,0.95)",
},
}}
onClick={fetchCredentials}
disabled={loading}
>
Refresh
</Button>
<Button
variant="contained"
size="small"
startIcon={<AddIcon />}
sx={{
...gradientButtonSx,
minWidth: 150,
}}
onClick={handleCreate}
>
New Credential
</Button>
</Stack>
</Box>
<Paper
sx={{
m: 0,
@@ -327,46 +375,6 @@ export default function CredentialList({ isAdmin = false, onPageMetaChange }) {
}}
elevation={0}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
px: 2,
pt: 1,
pb: 1,
}}
>
<Box sx={{ display: "flex", gap: 1 }}>
<Button
variant="outlined"
size="small"
startIcon={<RefreshIcon />}
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}
>
Refresh
</Button>
<Button
variant="contained"
size="small"
startIcon={<AddIcon />}
sx={gradientButtonSx}
onClick={handleCreate}
>
New Credential
</Button>
</Box>
</Box>
{loading && (
<Box
sx={{

View File

@@ -23,7 +23,8 @@ import {
FormControl,
InputLabel,
Checkbox,
Popover
Popover,
Stack
} from "@mui/material";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import FilterListIcon from "@mui/icons-material/FilterList";
@@ -454,15 +455,26 @@ export default function UserManagement({ isAdmin = false, onPageMetaChange }) {
return (
<>
<Paper sx={tablePaperSx} elevation={0}>
<Box sx={{ p: 2, pb: 1, display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 1 }}>
<Button
variant="contained"
size="small"
onClick={openCreate}
sx={gradientButtonSx}
>
Create User
</Button>
{/* Page-level action button (floating top-right) */}
<Box
sx={{
position: "fixed",
top: { xs: 72, md: 88 },
right: { xs: 12, md: 20 },
zIndex: 1400,
pointerEvents: "none",
}}
>
<Stack direction="row" spacing={1.25} alignItems="center" sx={{ pointerEvents: "auto" }}>
<Button
variant="contained"
size="small"
onClick={openCreate}
sx={gradientButtonSx}
>
Create User
</Button>
</Stack>
</Box>
<Table size="small" sx={tableSx}>

View File

@@ -359,8 +359,32 @@ const defaultColDef = useMemo(
flexDirection: "column",
}}
>
<Box
sx={{
position: "fixed",
top: { xs: 72, md: 88 },
right: { xs: 12, md: 20 },
zIndex: 1400,
pointerEvents: "none",
}}
>
<Stack direction="row" spacing={1.25} sx={{ pointerEvents: "auto" }}>
<Button
variant="contained"
startIcon={<RefreshIcon />}
onClick={() => {
fetchLogs();
if (selectedFile) fetchEntries(selectedFile);
}}
sx={{ ...gradientButtonSx, minWidth: 112 }}
>
Refresh
</Button>
</Stack>
</Box>
{!useGlobalHeader && (
<Box sx={{ px: 3, pt: 3, pb: 1, borderBottom: `1px solid ${AURORA_SHELL.border}` }}>
<Box sx={{ px: 3, pt: 3, pb: 1 }}>
<Stack direction="row" spacing={1.25} alignItems="center">
<LogsIcon sx={{ fontSize: 22, color: AURORA_SHELL.accent, mt: 0.25 }} />
<Typography
@@ -373,41 +397,12 @@ const defaultColDef = useMemo(
>
Log Management
</Typography>
<Box sx={{ flexGrow: 1 }} />
<Stack direction="row" spacing={1}>
<Button
variant="contained"
startIcon={<RefreshIcon />}
onClick={() => {
fetchLogs();
if (selectedFile) fetchEntries(selectedFile);
}}
sx={gradientButtonSx}
>
Refresh
</Button>
</Stack>
</Stack>
<Typography variant="body2" sx={{ color: AURORA_SHELL.muted, mt: 0.75, mb: 2.5 }}>
Analyze engine logs and adjust log retention periods for different engine services.
</Typography>
</Box>
)}
{useGlobalHeader && (
<Box sx={{ px: 3, pt: 2, pb: 1, display: "flex", justifyContent: "flex-end" }}>
<Button
variant="contained"
startIcon={<RefreshIcon />}
onClick={() => {
fetchLogs();
if (selectedFile) fetchEntries(selectedFile);
}}
sx={gradientButtonSx}
>
Refresh
</Button>
</Box>
)}
{error && (
<Box sx={{ px: 3, pt: 2 }}>
@@ -425,8 +420,8 @@ const defaultColDef = useMemo(
sx={{
width: 360,
p: 3,
borderRight: `1px solid ${AURORA_SHELL.border}`,
bgcolor: "rgba(3,7,18,0.7)",
borderRight: "none",
bgcolor: "transparent",
display: "flex",
flexDirection: "column",
gap: 2,
@@ -646,9 +641,32 @@ const defaultColDef = useMemo(
value={gridMode}
onChange={(_, val) => val && setGridMode(val)}
sx={{
backgroundColor: "rgba(0,0,0,0.25)",
borderRadius: 999,
p: 0.2,
background: "rgba(9,14,25,0.9)",
borderRadius: 1,
border: `1px solid ${AURORA_SHELL.border}`,
boxShadow: "0 14px 32px rgba(2,6,23,0.55)",
overflow: "hidden",
"& .MuiToggleButton-root": {
textTransform: "none",
color: "#dce7f5",
border: "none",
px: 2.8,
py: 1,
fontWeight: 700,
fontSize: 13,
letterSpacing: 0.1,
transition: "all 0.18s ease",
backgroundColor: "#0f1627",
"&:hover": { backgroundColor: "rgba(148,163,184,0.14)" },
"&.Mui-selected": {
color: "#0c1224",
backgroundImage: "linear-gradient(135deg,#7fc9ff 0%,#b195ff 100%)",
boxShadow: "0 10px 24px rgba(124,58,237,0.4)",
},
"&.Mui-selected:hover": {
backgroundImage: "linear-gradient(135deg,#8bd8ff 0%,#c0a8ff 100%)",
},
},
}}
>
<ToggleButton value="structured" sx={{ color: AURORA_SHELL.text, textTransform: "none" }}>
@@ -702,8 +720,8 @@ const defaultColDef = useMemo(
flexGrow: 1,
minHeight: 0,
borderRadius: 2,
border: `1px solid ${AURORA_SHELL.border}`,
bgcolor: "rgba(5,7,15,0.85)",
border: "none",
bgcolor: "transparent",
overflow: "hidden",
}}
>

View File

@@ -252,6 +252,46 @@ export default function PageTemplate({ onPageMetaChange }) {
}}
elevation={0}
>
<Box
sx={{
position: "fixed",
top: { xs: 72, md: 88 },
right: { xs: 12, md: 20 },
zIndex: 1400,
pointerEvents: "none",
}}
>
<Stack direction="row" spacing={1.25} sx={{ pointerEvents: "auto" }}>
<Tooltip title="New (example)">
<span>
<Button size="small" startIcon={<AddIcon />} sx={{ ...gradientButtonSx, minWidth: 120 }}>
New Item
</Button>
</span>
</Tooltip>
<Tooltip title="Settings (example)">
<span>
<Button
size="small"
variant="outlined"
startIcon={<TuneIcon />}
sx={{
borderColor: "rgba(148,163,184,0.35)",
color: "#e2e8f0",
textTransform: "none",
borderRadius: 999,
px: 1.7,
minWidth: 100,
"&:hover": { borderColor: "rgba(148,163,184,0.55)" },
}}
>
Settings
</Button>
</span>
</Tooltip>
</Stack>
</Box>
<Box
sx={{
px: 2,
@@ -280,33 +320,6 @@ export default function PageTemplate({ onPageMetaChange }) {
</Tooltip>
</Box>
<Stack direction="row" spacing={1}>
<Tooltip title="New (example)">
<span>
<Button size="small" startIcon={<AddIcon />} sx={gradientButtonSx}>
New Item
</Button>
</span>
</Tooltip>
<Tooltip title="Settings (example)">
<span>
<Button
size="small"
variant="outlined"
startIcon={<TuneIcon />}
sx={{
borderColor: "rgba(148,163,184,0.35)",
color: "#e2e8f0",
textTransform: "none",
borderRadius: 999,
px: 1.7, // ~5% wider than previous
minWidth: 86,
"&:hover": { borderColor: "rgba(148,163,184,0.55)" },
}}
>
Settings
</Button>
</span>
</Tooltip>
</Stack>
</Box>

View File

@@ -1,8 +1,12 @@
import React, { useEffect, useState } from "react";
import { Paper, Box, Typography, Button } from "@mui/material";
import React, { useEffect, useMemo, useState } from "react";
import { Paper, Box, Typography, Button, Stack } from "@mui/material";
import { GitHub as GitHubIcon, InfoOutlined as InfoIcon } from "@mui/icons-material";
import { AgGridReact } from "ag-grid-react";
import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community";
import { CreditsDialog } from "../Dialogs.jsx";
ModuleRegistry.registerModules([AllCommunityModule]);
const gradientButtonSx = {
backgroundImage: "linear-gradient(135deg,#7dd3fc,#c084fc)",
color: "#0b1220",
@@ -16,6 +20,19 @@ const gradientButtonSx = {
},
};
const gridTheme = themeQuartz.withParams({
accentColor: "#7dd3fc",
backgroundColor: "#050915",
browserColorScheme: "dark",
fontFamily: { googleFont: "IBM Plex Sans" },
foregroundColor: "#e2e8f0",
headerFontSize: 13,
});
const themeClassName = gridTheme.themeName || "ag-theme-quartz";
const gridFontFamily = "\"IBM Plex Sans\", \"Helvetica Neue\", Arial, sans-serif";
const iconFontFamily = "\"Quartz Regular\"";
export default function ServerInfo({ isAdmin = false, onPageMetaChange }) {
const [serverTime, setServerTime] = useState(null);
const [error, setError] = useState(null);
@@ -51,50 +68,168 @@ export default function ServerInfo({ isAdmin = false, onPageMetaChange }) {
return () => onPageMetaChange?.(null);
}, [onPageMetaChange]);
const infoRows = useMemo(
() => [
{
id: "server-time",
field: "Server Time",
value: error ? `Error: ${error}` : (serverTime || "Loading..."),
description: "Internal server clock used for troubleshooting the job scheduling system.",
},
],
[error, serverTime]
);
const columnDefs = useMemo(
() => [
{ headerName: "Field", field: "field", minWidth: 180, flex: 0.35 },
{ headerName: "Value", field: "value", minWidth: 240, flex: 0.4 },
{ headerName: "Description", field: "description", minWidth: 260, flex: 0.6 },
],
[]
);
if (!isAdmin) return null;
return (
<Paper sx={{ m: 0, p: 0, bgcolor: "transparent", border: "none", boxShadow: "none" }} elevation={0}>
<Box sx={{ p: 2 }}>
<Box
sx={{
position: "fixed",
top: { xs: 72, md: 88 },
right: { xs: 12, md: 20 },
zIndex: 1400,
pointerEvents: "none",
}}
>
<Stack direction="row" spacing={1.25} sx={{ pointerEvents: "auto" }}>
<Button
variant="outlined"
startIcon={<GitHubIcon />}
onClick={() => window.open("https://github.com/bunny-lab-io/Borealis", "_blank")}
sx={{
borderColor: "rgba(148,163,184,0.4)",
color: "#e2e8f0",
textTransform: "none",
borderRadius: 999,
px: 1.9,
minWidth: 150,
backgroundColor: "rgba(12,18,35,0.85)",
"&:hover": {
borderColor: "rgba(125,211,252,0.85)",
backgroundColor: "rgba(16,24,44,0.95)",
},
}}
>
GitHub Project
</Button>
<Button
variant="contained"
startIcon={<InfoIcon />}
onClick={() => setAboutOpen(true)}
sx={{ ...gradientButtonSx, minWidth: 150 }}
>
About Borealis
</Button>
</Stack>
</Box>
<Box
sx={{
p: 2,
pb: 3,
display: "flex",
flexDirection: "column",
minHeight: "calc(100vh - 110px)",
gap: 2,
}}
>
<Typography sx={{ color: '#aaa', mb: 1 }}>
Basic server information for debug and support. Server time updates automatically every minute.
</Typography>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'baseline' }}>
<Typography sx={{ color: '#ccc', fontWeight: 600, minWidth: 120 }}>Server Time</Typography>
<Typography sx={{ color: error ? '#ff6b6b' : '#ddd', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' }}>
{error ? `Error: ${error}` : (serverTime || 'Loading...')}
</Typography>
<Box
className={themeClassName}
sx={{
mt: 3,
width: "100%",
maxWidth: "100%",
minHeight: "75vh",
display: "flex",
flexDirection: "column",
background: "rgba(8,12,24,0.72)",
borderRadius: 2,
border: "1px solid rgba(148,163,184,0.35)",
boxShadow: "0 12px 36px rgba(2,6,23,0.55)",
overflow: "hidden",
fontFamily: gridFontFamily,
"--ag-font-family": gridFontFamily,
"--ag-icon-font-family": iconFontFamily,
"& .ag-root-wrapper": {
border: "none",
borderRadius: 2,
},
"& .ag-center-cols-container .ag-cell, & .ag-pinned-left-cols-container .ag-cell, & .ag-pinned-right-cols-container .ag-cell": {
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
textAlign: "left",
padding: "8px 12px 8px 18px",
},
"& .ag-center-cols-container .ag-cell .ag-cell-wrapper, & .ag-pinned-left-cols-container .ag-cell .ag-cell-wrapper, & .ag-pinned-right-cols-container .ag-cell .ag-cell-wrapper": {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
padding: 0,
},
"& .ag-header": {
borderBottom: "1px solid rgba(148,163,184,0.35)",
},
}}
style={{
"--ag-background-color": "rgba(5,9,21,0.92)",
"--ag-foreground-color": "#e2e8f0",
"--ag-header-background-color": "#0b1226",
"--ag-header-foreground-color": "#cfe0ff",
"--ag-row-hover-color": "rgba(125,211,252,0.08)",
"--ag-selected-row-background-color": "rgba(64,164,255,0.16)",
"--ag-border-color": "rgba(148,163,184,0.28)",
"--ag-row-border-color": "rgba(148,163,184,0.16)",
"--ag-odd-row-background-color": "rgba(255,255,255,0.02)",
"--ag-border-radius": "12px",
}}
>
<AgGridReact
rowData={infoRows}
columnDefs={columnDefs}
defaultColDef={{
sortable: false,
filter: false,
resizable: true,
flex: 1,
minWidth: 160,
cellStyle: {
color: "#e2e8f0",
fontFamily: gridFontFamily,
fontSize: 14,
},
headerClass: "server-info-grid-header",
}}
getRowId={(params) => params.data?.id || String(params.rowIndex ?? "")}
suppressCellFocus
animateRows
rowHeight={48}
headerHeight={42}
pagination={false}
theme={gridTheme}
style={{
width: "100%",
flex: 1,
fontFamily: gridFontFamily,
"--ag-icon-font-family": iconFontFamily,
}}
/>
</Box>
<Box sx={{ mt: 3 }}>
<Typography variant="subtitle1" sx={{ color: "#e2e8f0", mb: 1, fontWeight: 600 }}>Project Links</Typography>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<Button
variant="outlined"
startIcon={<GitHubIcon />}
onClick={() => window.open("https://github.com/bunny-lab-io/Borealis", "_blank")}
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)" },
}}
>
GitHub Project
</Button>
<Button
variant="contained"
startIcon={<InfoIcon />}
onClick={() => setAboutOpen(true)}
sx={gradientButtonSx}
>
About Borealis
</Button>
</Box>
</Box>
</Box>
<CreditsDialog open={aboutOpen} onClose={() => setAboutOpen(false)} />
</Paper>

View File

@@ -14,6 +14,7 @@ import {
TextField,
CircularProgress,
Link as MuiLink,
Stack,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import CachedIcon from "@mui/icons-material/Cached";
@@ -633,6 +634,40 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript, userRole =
}}
elevation={0}
>
{/* Page-level action button (floating top-right) */}
<Box
sx={{
position: "fixed",
top: { xs: 72, md: 88 },
right: { xs: 12, md: 20 },
zIndex: 1400,
pointerEvents: "none",
}}
>
<Stack direction="row" spacing={1.25} alignItems="center" sx={{ pointerEvents: "auto" }}>
<Button
variant="contained"
size="small"
startIcon={<AddIcon />}
onClick={(event) => setNewMenuAnchor(event.currentTarget)}
sx={{
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",
},
}}
>
New Assembly
</Button>
</Stack>
</Box>
<Box sx={{ px: 2, mt: 1, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1 }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<IconButton
@@ -653,26 +688,6 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript, userRole =
</Typography>
) : null}
</Box>
<Button
variant="contained"
size="small"
startIcon={<AddIcon />}
onClick={(event) => setNewMenuAnchor(event.currentTarget)}
sx={{
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",
},
}}
>
New Assembly
</Button>
<Menu
anchorEl={newMenuAnchor}
open={Boolean(newMenuAnchor)}

View File

@@ -22,8 +22,7 @@ import {
import {
Schedule as HeaderIcon,
Cached as CachedIcon,
Add as AddIcon,
Tune as TuneIcon
Add as AddIcon
} from "@mui/icons-material";
import { AgGridReact } from "ag-grid-react";
import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community";
@@ -746,45 +745,36 @@ export default function ScheduledJobsList({ onCreateJob, onEditJob, refreshToken
}}
elevation={0}
>
<Box sx={{ px: 3, pt: 3, pb: 1, display: "flex", justifyContent: "flex-end", gap: 1.5 }}>
<Tooltip title="Refresh">
<span>
<IconButton
size="small"
onClick={handleRefreshClick}
sx={{ color: "#cbd5e1", borderRadius: 1, "&:hover": { color: "#ffffff" } }}
>
<CachedIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Create Job">
<span>
<Button size="small" startIcon={<AddIcon />} sx={gradientButtonSx} onClick={() => onCreateJob && onCreateJob()}>
Create Job
</Button>
</span>
</Tooltip>
<Tooltip title="Settings">
<span>
<Button
size="small"
variant="outlined"
startIcon={<TuneIcon />}
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)" },
}}
>
Settings
</Button>
</span>
</Tooltip>
{/* Page-level action buttons (floating top-right) */}
<Box
sx={{
position: "fixed",
top: { xs: 72, md: 88 },
right: { xs: 12, md: 20 },
zIndex: 1400,
pointerEvents: "none",
}}
>
<Stack direction="row" spacing={1} alignItems="center" sx={{ pointerEvents: "auto" }}>
<Tooltip title="Refresh">
<span>
<IconButton
size="small"
onClick={handleRefreshClick}
sx={{ color: "#cbd5e1", borderRadius: 1, "&:hover": { color: "#ffffff" } }}
>
<CachedIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Create Job">
<span>
<Button size="small" startIcon={<AddIcon />} sx={gradientButtonSx} onClick={() => onCreateJob && onCreateJob()}>
Create Job
</Button>
</span>
</Tooltip>
</Stack>
</Box>
<Box sx={{ mt: 2, px: 2, pb: 2, flexGrow: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>

View File

@@ -3,7 +3,7 @@
Borealis is a remote management platform with a simple, visual automation layer, enabling you to leverage scripts and advanced nodegraph-based automation workflows. I originally created Borealis to work towards consolidating the core functionality of several standalone automation platforms in my homelab, such as TacticalRMM, Ansible AWX, SemaphoreUI, and a few others.
### A Note on Development Pace
I'm the sole maintainer and still learning as I go, while working a full-time IT job. Progress is sporadic, and parts of the codebase get rebuilt when I discover better or more optimized approaches. Thank you for your patience with the slower cadence. Ko-Fi donations are always welcome and help keep m,me motivated to continue development of Borealis.
I'm the sole maintainer and still learning as I go, while working a full-time IT job. Progress is sporadic, and parts of the codebase get rebuilt when I discover better or more optimized approaches. Thank you for your patience with the slower cadence. Ko-Fi donations are always welcome and help keep me motivated to actively continue development of Borealis.
---