mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 21:41:57 -06:00
Merge pull request #99 from bunny-lab-io:codex/update-tables-to-use-ag-grid-design
Refactor credential list table to AG Grid
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -6,14 +6,8 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Paper,
|
Paper,
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableSortLabel,
|
|
||||||
Typography,
|
Typography,
|
||||||
CircularProgress
|
CircularProgress,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
@@ -21,31 +15,32 @@ import RefreshIcon from "@mui/icons-material/Refresh";
|
|||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
import WifiIcon from "@mui/icons-material/Wifi";
|
import WifiIcon from "@mui/icons-material/Wifi";
|
||||||
import ComputerIcon from "@mui/icons-material/Computer";
|
import ComputerIcon from "@mui/icons-material/Computer";
|
||||||
|
import { AgGridReact } from "ag-grid-react";
|
||||||
|
import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community";
|
||||||
import CredentialEditor from "./Credential_Editor.jsx";
|
import CredentialEditor from "./Credential_Editor.jsx";
|
||||||
import { ConfirmDeleteDialog } from "../Dialogs.jsx";
|
import { ConfirmDeleteDialog } from "../Dialogs.jsx";
|
||||||
|
|
||||||
const tablePaperSx = { m: 2, p: 0, bgcolor: "#1e1e1e", borderRadius: 2 };
|
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||||
const tableSx = {
|
|
||||||
minWidth: 840,
|
|
||||||
"& th, & td": {
|
|
||||||
color: "#ddd",
|
|
||||||
borderColor: "#2a2a2a",
|
|
||||||
fontSize: 13,
|
|
||||||
py: 0.9
|
|
||||||
},
|
|
||||||
"& th .MuiTableSortLabel-root": { color: "#ddd" },
|
|
||||||
"& th .MuiTableSortLabel-root.Mui-active": { color: "#ddd" }
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
const myTheme = themeQuartz.withParams({
|
||||||
{ id: "name", label: "Name" },
|
accentColor: "#FFA6FF",
|
||||||
{ id: "credential_type", label: "Credential Type" },
|
backgroundColor: "#1f2836",
|
||||||
{ id: "connection_type", label: "Connection" },
|
browserColorScheme: "dark",
|
||||||
{ id: "site_name", label: "Site" },
|
chromeBackgroundColor: {
|
||||||
{ id: "username", label: "Username" },
|
ref: "foregroundColor",
|
||||||
{ id: "updated_at", label: "Updated" },
|
mix: 0.07,
|
||||||
{ id: "actions", label: "" }
|
onto: "backgroundColor"
|
||||||
];
|
},
|
||||||
|
fontFamily: {
|
||||||
|
googleFont: "IBM Plex Sans"
|
||||||
|
},
|
||||||
|
foregroundColor: "#FFF",
|
||||||
|
headerFontSize: 14
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeClassName = myTheme.themeName || "ag-theme-quartz";
|
||||||
|
const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif';
|
||||||
|
const iconFontFamily = '"Quartz Regular"';
|
||||||
|
|
||||||
function formatTs(ts) {
|
function formatTs(ts) {
|
||||||
if (!ts) return "-";
|
if (!ts) return "-";
|
||||||
@@ -69,8 +64,6 @@ function connectionIcon(connection) {
|
|||||||
|
|
||||||
export default function CredentialList({ isAdmin = false }) {
|
export default function CredentialList({ isAdmin = false }) {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
const [orderBy, setOrderBy] = useState("name");
|
|
||||||
const [order, setOrder] = useState("asc");
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [menuAnchor, setMenuAnchor] = useState(null);
|
const [menuAnchor, setMenuAnchor] = useState(null);
|
||||||
@@ -80,18 +73,126 @@ export default function CredentialList({ isAdmin = false }) {
|
|||||||
const [editingCredential, setEditingCredential] = useState(null);
|
const [editingCredential, setEditingCredential] = useState(null);
|
||||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||||
const [deleteBusy, setDeleteBusy] = useState(false);
|
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||||
|
const gridApiRef = useRef(null);
|
||||||
|
|
||||||
const sortedRows = useMemo(() => {
|
const openMenu = useCallback((event, row) => {
|
||||||
const sorted = [...rows];
|
setMenuAnchor(event.currentTarget);
|
||||||
sorted.sort((a, b) => {
|
setMenuRow(row);
|
||||||
const aVal = (a?.[orderBy] ?? "").toString().toLowerCase();
|
}, []);
|
||||||
const bVal = (b?.[orderBy] ?? "").toString().toLowerCase();
|
|
||||||
if (aVal < bVal) return order === "asc" ? -1 : 1;
|
const closeMenu = useCallback(() => {
|
||||||
if (aVal > bVal) return order === "asc" ? 1 : -1;
|
setMenuAnchor(null);
|
||||||
return 0;
|
setMenuRow(null);
|
||||||
});
|
}, []);
|
||||||
return sorted;
|
|
||||||
}, [rows, order, orderBy]);
|
const connectionCellRenderer = useCallback((params) => {
|
||||||
|
const row = params.data || {};
|
||||||
|
const label = titleCase(row.connection_type);
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", fontFamily: gridFontFamily }}>
|
||||||
|
{connectionIcon(row.connection_type)}
|
||||||
|
<Box component="span" sx={{ color: "#f5f7fa" }}>
|
||||||
|
{label}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const actionCellRenderer = useCallback(
|
||||||
|
(params) => {
|
||||||
|
const row = params.data;
|
||||||
|
if (!row) return null;
|
||||||
|
const handleClick = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
openMenu(event, row);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<IconButton size="small" onClick={handleClick} sx={{ color: "#7db7ff" }}>
|
||||||
|
<MoreVertIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[openMenu]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnDefs = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
headerName: "Name",
|
||||||
|
field: "name",
|
||||||
|
sort: "asc",
|
||||||
|
cellRenderer: (params) => params.value || "-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: "Credential Type",
|
||||||
|
field: "credential_type",
|
||||||
|
valueGetter: (params) => titleCase(params.data?.credential_type)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: "Connection",
|
||||||
|
field: "connection_type",
|
||||||
|
cellRenderer: connectionCellRenderer
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: "Site",
|
||||||
|
field: "site_name",
|
||||||
|
cellRenderer: (params) => params.value || "-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: "Username",
|
||||||
|
field: "username",
|
||||||
|
cellRenderer: (params) => params.value || "-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: "Updated",
|
||||||
|
field: "updated_at",
|
||||||
|
valueGetter: (params) =>
|
||||||
|
formatTs(params.data?.updated_at || params.data?.created_at)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: "",
|
||||||
|
field: "__actions__",
|
||||||
|
minWidth: 70,
|
||||||
|
maxWidth: 80,
|
||||||
|
sortable: false,
|
||||||
|
filter: false,
|
||||||
|
resizable: false,
|
||||||
|
suppressMenu: true,
|
||||||
|
cellRenderer: actionCellRenderer,
|
||||||
|
pinned: "right"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[actionCellRenderer, connectionCellRenderer]
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultColDef = useMemo(
|
||||||
|
() => ({
|
||||||
|
sortable: true,
|
||||||
|
filter: "agTextColumnFilter",
|
||||||
|
resizable: true,
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 140,
|
||||||
|
cellStyle: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
color: "#f5f7fa",
|
||||||
|
fontFamily: gridFontFamily,
|
||||||
|
fontSize: "13px"
|
||||||
|
},
|
||||||
|
headerClass: "credential-grid-header"
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRowId = useCallback(
|
||||||
|
(params) =>
|
||||||
|
params.data?.id ||
|
||||||
|
params.data?.name ||
|
||||||
|
params.data?.username ||
|
||||||
|
String(params.rowIndex ?? ""),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const fetchCredentials = useCallback(async () => {
|
const fetchCredentials = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -115,25 +216,6 @@ export default function CredentialList({ isAdmin = false }) {
|
|||||||
fetchCredentials();
|
fetchCredentials();
|
||||||
}, [fetchCredentials]);
|
}, [fetchCredentials]);
|
||||||
|
|
||||||
const handleSort = (columnId) => () => {
|
|
||||||
if (orderBy === columnId) {
|
|
||||||
setOrder((prev) => (prev === "asc" ? "desc" : "asc"));
|
|
||||||
} else {
|
|
||||||
setOrderBy(columnId);
|
|
||||||
setOrder("asc");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openMenu = (event, row) => {
|
|
||||||
setMenuAnchor(event.currentTarget);
|
|
||||||
setMenuRow(row);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
setMenuAnchor(null);
|
|
||||||
setMenuRow(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
setEditorMode("create");
|
setEditorMode("create");
|
||||||
setEditingCredential(null);
|
setEditingCredential(null);
|
||||||
@@ -176,6 +258,22 @@ export default function CredentialList({ isAdmin = false }) {
|
|||||||
await fetchCredentials();
|
await fetchCredentials();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGridReady = useCallback((params) => {
|
||||||
|
gridApiRef.current = params.api;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const api = gridApiRef.current;
|
||||||
|
if (!api) return;
|
||||||
|
if (loading) {
|
||||||
|
api.showLoadingOverlay();
|
||||||
|
} else if (!rows.length) {
|
||||||
|
api.showNoRowsOverlay();
|
||||||
|
} else {
|
||||||
|
api.hideOverlay();
|
||||||
|
}
|
||||||
|
}, [loading, rows]);
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ m: 2, p: 3, bgcolor: "#1e1e1e" }}>
|
<Paper sx={{ m: 2, p: 3, bgcolor: "#1e1e1e" }}>
|
||||||
@@ -191,8 +289,30 @@ export default function CredentialList({ isAdmin = false }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Paper sx={tablePaperSx} elevation={3}>
|
<Paper
|
||||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", p: 2, borderBottom: "1px solid #2a2a2a" }}>
|
sx={{
|
||||||
|
m: 2,
|
||||||
|
p: 0,
|
||||||
|
bgcolor: "#1e1e1e",
|
||||||
|
fontFamily: gridFontFamily,
|
||||||
|
color: "#f5f7fa",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flexGrow: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
minHeight: 420
|
||||||
|
}}
|
||||||
|
elevation={2}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
p: 2,
|
||||||
|
borderBottom: "1px solid #2a2a2a"
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0.3 }}>
|
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0.3 }}>
|
||||||
Credentials
|
Credentials
|
||||||
@@ -224,69 +344,93 @@ export default function CredentialList({ isAdmin = false }) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{loading && (
|
{loading && (
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, color: "#7db7ff", px: 2, py: 1.5 }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
color: "#7db7ff",
|
||||||
|
px: 2,
|
||||||
|
py: 1.5,
|
||||||
|
borderBottom: "1px solid #2a2a2a"
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CircularProgress size={18} sx={{ color: "#58a6ff" }} />
|
<CircularProgress size={18} sx={{ color: "#58a6ff" }} />
|
||||||
<Typography variant="body2">Loading credentials…</Typography>
|
<Typography variant="body2">Loading credentials…</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<Box sx={{ px: 2, py: 1.5, color: "#ff8080" }}>
|
<Box sx={{ px: 2, py: 1.5, color: "#ff8080", borderBottom: "1px solid #2a2a2a" }}>
|
||||||
<Typography variant="body2">{error}</Typography>
|
<Typography variant="body2">{error}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Table size="small" sx={tableSx}>
|
<Box
|
||||||
<TableHead>
|
sx={{
|
||||||
<TableRow>
|
flexGrow: 1,
|
||||||
{columns.map((col) => (
|
minHeight: 0,
|
||||||
<TableCell key={col.id} align={col.id === "actions" ? "right" : "left"}>
|
display: "flex",
|
||||||
{col.id === "actions" ? null : (
|
flexDirection: "column",
|
||||||
<TableSortLabel
|
mt: "10px",
|
||||||
active={orderBy === col.id}
|
px: 2,
|
||||||
direction={orderBy === col.id ? order : "asc"}
|
pb: 2
|
||||||
onClick={handleSort(col.id)}
|
}}
|
||||||
>
|
>
|
||||||
{col.label}
|
<Box
|
||||||
</TableSortLabel>
|
className={themeClassName}
|
||||||
)}
|
sx={{
|
||||||
</TableCell>
|
width: "100%",
|
||||||
))}
|
height: "100%",
|
||||||
</TableRow>
|
flexGrow: 1,
|
||||||
</TableHead>
|
fontFamily: gridFontFamily,
|
||||||
<TableBody>
|
"--ag-font-family": gridFontFamily,
|
||||||
{!sortedRows.length && !loading ? (
|
"--ag-icon-font-family": iconFontFamily,
|
||||||
<TableRow>
|
"--ag-row-border-style": "solid",
|
||||||
<TableCell colSpan={columns.length} sx={{ color: "#888", textAlign: "center", py: 4 }}>
|
"--ag-row-border-color": "#2a2a2a",
|
||||||
No credentials have been created yet.
|
"--ag-row-border-width": "1px",
|
||||||
</TableCell>
|
"& .ag-root-wrapper": {
|
||||||
</TableRow>
|
borderRadius: 1,
|
||||||
) : (
|
minHeight: 320
|
||||||
sortedRows.map((row) => (
|
},
|
||||||
<TableRow key={row.id} hover>
|
"& .ag-root, & .ag-header, & .ag-center-cols-container, & .ag-paging-panel": {
|
||||||
<TableCell>{row.name || "-"}</TableCell>
|
fontFamily: gridFontFamily
|
||||||
<TableCell>{titleCase(row.credential_type)}</TableCell>
|
},
|
||||||
<TableCell>
|
"& .ag-icon": {
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
fontFamily: iconFontFamily
|
||||||
{connectionIcon(row.connection_type)}
|
}
|
||||||
{titleCase(row.connection_type)}
|
}}
|
||||||
|
style={{ color: "#f5f7fa" }}
|
||||||
|
>
|
||||||
|
<AgGridReact
|
||||||
|
rowData={rows}
|
||||||
|
columnDefs={columnDefs}
|
||||||
|
defaultColDef={defaultColDef}
|
||||||
|
animateRows
|
||||||
|
rowHeight={46}
|
||||||
|
headerHeight={44}
|
||||||
|
getRowId={getRowId}
|
||||||
|
overlayNoRowsTemplate="<span class='ag-overlay-no-rows-center'>No credentials have been created yet.</span>"
|
||||||
|
onGridReady={handleGridReady}
|
||||||
|
suppressCellFocus
|
||||||
|
theme={myTheme}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
fontFamily: gridFontFamily,
|
||||||
|
"--ag-icon-font-family": iconFontFamily
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
|
||||||
<TableCell>{row.site_name || "-"}</TableCell>
|
|
||||||
<TableCell>{row.username || "-"}</TableCell>
|
|
||||||
<TableCell>{formatTs(row.updated_at || row.created_at)}</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
<IconButton size="small" onClick={(e) => openMenu(e, row)} sx={{ color: "#7db7ff" }}>
|
|
||||||
<MoreVertIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Menu anchorEl={menuAnchor} open={Boolean(menuAnchor)} onClose={closeMenu} elevation={2} PaperProps={{ sx: { bgcolor: "#1f1f1f", color: "#f5f5f5" } }}>
|
<Menu
|
||||||
|
anchorEl={menuAnchor}
|
||||||
|
open={Boolean(menuAnchor)}
|
||||||
|
onClose={closeMenu}
|
||||||
|
elevation={2}
|
||||||
|
PaperProps={{ sx: { bgcolor: "#1f1f1f", color: "#f5f5f5" } }}
|
||||||
|
>
|
||||||
<MenuItem onClick={() => handleEdit(menuRow)}>Edit</MenuItem>
|
<MenuItem onClick={() => handleEdit(menuRow)}>Edit</MenuItem>
|
||||||
<MenuItem onClick={() => handleDelete(menuRow)} sx={{ color: "#ff8080" }}>
|
<MenuItem onClick={() => handleDelete(menuRow)} sx={{ color: "#ff8080" }}>
|
||||||
Delete
|
Delete
|
||||||
|
|||||||
Reference in New Issue
Block a user