feat(admin): show deployed app version
CI and Deploy / test (pull_request) Successful in 4m48s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 16:47:43 +02:00
parent 6b02e3f5d1
commit a6cffe0473
9 changed files with 175 additions and 22 deletions
+3
View File
@@ -78,6 +78,8 @@ type MeResponse = {
roles?: string[];
plan?: "free" | "pro";
entitlements?: { ai?: boolean; proThemes?: boolean };
appVersion?: string;
appCommitSha?: string;
};
function breadcrumbsFor(path: string, t: (k: any) => string): string[] {
@@ -348,6 +350,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
onToggleDrawer={setMobileDrawerOpen}
onNavigate={(to) => { setMobileDrawerOpen(false); navigate(to); }}
user={{ email: me?.email, userName: me?.userName, displayName: me?.displayName || fullName || undefined, avatarImageDataUrl: me?.avatarImageDataUrl, roleLabel: isAdmin ? t("superAdmin") : t("user") }}
buildMetadata={isAdmin && me?.appVersion ? { version: me.appVersion, commitSha: me.appCommitSha } : undefined}
notificationsCount={notificationCount}
onOpenNotifications={(anchor) => setNotificationAnchor(anchor)}
onOpenSettings={() => navigate("/settings")}
@@ -36,3 +36,41 @@ test("notification bell exposes its unread count and keyboard-accessible action"
expect(open).toHaveBeenCalledTimes(1);
expect(open.mock.calls[0][0]).toBeInstanceOf(HTMLElement);
});
test("shows build metadata only when the caller supplies the admin-only badge", () => {
const commonProps = {
pageTitle: "Dashboard",
breadcrumbs: ["Home"],
pathname: "/dashboard",
nav: [],
navBottom: [],
onNavigate: () => undefined,
onToggleDrawer: () => undefined,
drawerOpen: false,
};
const { rerender } = render(
<CssVarsProvider theme={getTheme("light") as any} defaultMode="light">
<I18nProvider>
<AppShell {...commonProps} buildMetadata={{ version: "2.4.1", commitSha: "abc1234" }}>
<div>Content</div>
</AppShell>
</I18nProvider>
</CssVarsProvider>,
);
expect(screen.getByTestId("admin-version-badge")).toHaveTextContent("v2.4.1");
expect(screen.getByLabelText("Application version 2.4.1, commit abc1234")).toBeInTheDocument();
rerender(
<CssVarsProvider theme={getTheme("light") as any} defaultMode="light">
<I18nProvider>
<AppShell {...commonProps}>
<div>Content</div>
</AppShell>
</I18nProvider>
</CssVarsProvider>,
);
expect(screen.queryByTestId("admin-version-badge")).not.toBeInTheDocument();
});
+26
View File
@@ -6,6 +6,7 @@ import {
Badge,
Box,
Breadcrumbs,
Chip,
Divider,
Drawer,
IconButton,
@@ -16,6 +17,7 @@ import {
Menu,
MenuItem,
Toolbar,
Tooltip,
Typography,
} from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery";
@@ -82,6 +84,7 @@ export default function AppShell({
onToggleDrawer,
drawerOpen,
user,
buildMetadata,
notificationsCount,
onOpenNotifications,
onOpenSettings,
@@ -100,6 +103,7 @@ export default function AppShell({
onToggleDrawer: (open: boolean) => void;
drawerOpen: boolean;
user?: { email?: string; userName?: string; displayName?: string; avatarImageDataUrl?: string; roleLabel?: string };
buildMetadata?: { version: string; commitSha?: string };
notificationsCount?: number;
onOpenNotifications?: (anchorEl: HTMLElement) => void;
onOpenSettings?: () => void;
@@ -234,6 +238,26 @@ export default function AppShell({
const nameForAvatar = user?.userName || user?.displayName || user?.email;
const initials = initialsFrom(nameForAvatar);
const buildVersion = buildMetadata?.version.trim();
const buildCommit = buildMetadata?.commitSha?.trim();
const buildBadge = buildVersion ? (
<Tooltip title={buildCommit ? `Version ${buildVersion} • commit ${buildCommit}` : `Version ${buildVersion}`}>
<Chip
data-testid="admin-version-badge"
aria-label={`Application version ${buildVersion}${buildCommit ? `, commit ${buildCommit}` : ""}`}
label={`v${buildVersion}`}
size="small"
variant="outlined"
sx={{
maxWidth: 150,
fontWeight: 700,
fontVariantNumeric: "tabular-nums",
bgcolor: "background.paper",
"& .MuiChip-label": { overflow: "hidden", textOverflow: "ellipsis" },
}}
/>
</Tooltip>
) : null;
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
const userMenuOpen = Boolean(userMenuAnchor);
@@ -281,6 +305,7 @@ export default function AppShell({
Jobbjakt
</Typography>
</Box>
{buildBadge}
</Box>
{user ? (
@@ -358,6 +383,7 @@ export default function AppShell({
flex: { xs: "1 1 100%", md: "0 1 auto" },
}}
>
{buildBadge}
<IconButton
color="secondary"
size="small"