126 lines
4.8 KiB
JavaScript
126 lines
4.8 KiB
JavaScript
import Alert from "@mui/material/Alert";
|
|
import Box from "@mui/material/Box";
|
|
import Button from "@mui/material/Button";
|
|
import Card from "@mui/material/Card";
|
|
import CardContent from "@mui/material/CardContent";
|
|
import Chip from "@mui/material/Chip";
|
|
import Skeleton from "@mui/material/Skeleton";
|
|
import Stack from "@mui/material/Stack";
|
|
import Table from "@mui/material/Table";
|
|
import TableBody from "@mui/material/TableBody";
|
|
import TableCell from "@mui/material/TableCell";
|
|
import TableContainer from "@mui/material/TableContainer";
|
|
import TableHead from "@mui/material/TableHead";
|
|
import TableRow from "@mui/material/TableRow";
|
|
import Typography from "@mui/material/Typography";
|
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
import { useTheme } from "@mui/material/styles";
|
|
import { useExternalI18n } from "../i18n/ExternalI18nProvider.jsx";
|
|
import { translateExternalError } from "../i18n/errors.js";
|
|
|
|
export function ExternalPage({ actions, children, title }) {
|
|
return (
|
|
<Box className="external-page">
|
|
<Stack alignItems={{ md: "center" }} direction={{ xs: "column", md: "row" }} justifyContent="space-between" spacing={2}>
|
|
<Typography component="h1" variant="h5">
|
|
{title}
|
|
</Typography>
|
|
{actions ? <Stack direction="row" flexWrap="wrap" gap={1}>{actions}</Stack> : null}
|
|
</Stack>
|
|
{children}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export function ExternalPageState({ error, loading, onRetry }) {
|
|
const { t } = useExternalI18n();
|
|
if (loading) {
|
|
return (
|
|
<Box aria-label={t("common.loading")} className="external-skeleton" role="status">
|
|
<Skeleton height={46} variant="rounded" />
|
|
<Skeleton height="min(52vh, 520px)" variant="rounded" />
|
|
</Box>
|
|
);
|
|
}
|
|
if (error) {
|
|
return (
|
|
<Stack spacing={2}>
|
|
<Alert severity="error">{translateExternalError(error, t, "errors.loadFailed")}</Alert>
|
|
<Button onClick={onRetry} variant="outlined">{t("common.reload")}</Button>
|
|
</Stack>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function ResponsiveDataList({ columns, emptyText, items, rowKey }) {
|
|
const theme = useTheme();
|
|
const mobile = useMediaQuery(theme.breakpoints.down("sm"));
|
|
const { t } = useExternalI18n();
|
|
|
|
if (!items.length) {
|
|
return <Box className="external-empty">{emptyText || t("common.noData")}</Box>;
|
|
}
|
|
|
|
if (mobile) {
|
|
return (
|
|
<Stack spacing={1.5}>
|
|
{items.map((item, index) => (
|
|
<Card key={rowKey(item, index)} variant="outlined">
|
|
<CardContent className="external-mobile-card">
|
|
{columns.map((column) => (
|
|
<Box className="external-mobile-row" key={column.key}>
|
|
<Typography color="text.secondary" component="span" variant="body2">{column.label}</Typography>
|
|
<Box>{column.render(item)}</Box>
|
|
</Box>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TableContainer className="external-table-container">
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>{columns.map((column) => <TableCell key={column.key}>{column.label}</TableCell>)}</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{items.map((item, index) => (
|
|
<TableRow hover key={rowKey(item, index)}>
|
|
{columns.map((column) => <TableCell key={column.key}>{column.render(item)}</TableCell>)}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
);
|
|
}
|
|
|
|
export function StatusChip({ status }) {
|
|
const { t } = useExternalI18n();
|
|
const normalized = String(status || "-").toLowerCase();
|
|
const enabled = ["active", "enabled", "running", "normal", "unbanned"].includes(normalized);
|
|
const dangerous = ["banned", "disabled", "failed", "blocked"].includes(normalized);
|
|
const statusKey = `status.${normalized}`;
|
|
const translated = t(statusKey);
|
|
return <Chip color={enabled ? "success" : dangerous ? "error" : "default"} label={translated === statusKey ? status || "-" : translated} size="small" variant="outlined" />;
|
|
}
|
|
|
|
export function PagePagination({ page, pageSize, total, onChange }) {
|
|
const { t } = useExternalI18n();
|
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
return (
|
|
<Stack alignItems="center" direction="row" justifyContent="space-between" spacing={1}>
|
|
<Typography color="text.secondary" variant="body2">{t("pagination.total", { count: total })}</Typography>
|
|
<Stack direction="row" spacing={1}>
|
|
<Button disabled={page <= 1} onClick={() => onChange(page - 1)} variant="outlined">{t("pagination.previous")}</Button>
|
|
<Typography className="external-page-number" variant="body2">{page} / {totalPages}</Typography>
|
|
<Button disabled={page >= totalPages} onClick={() => onChange(page + 1)} variant="outlined">{t("pagination.next")}</Button>
|
|
</Stack>
|
|
</Stack>
|
|
);
|
|
}
|