diff --git a/frontend/src/app/(app)/layout/horizontal/header/Header.tsx b/frontend/src/app/(app)/layout/horizontal/header/Header.tsx index cc027c1..7be73c1 100644 --- a/frontend/src/app/(app)/layout/horizontal/header/Header.tsx +++ b/frontend/src/app/(app)/layout/horizontal/header/Header.tsx @@ -9,8 +9,8 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import { styled } from '@mui/material/styles'; import { IconMenu2 } from "@tabler/icons-react"; +import About from "../../vertical/header/About"; import ApiDocsLink from "../../vertical/header/ApiDocsLink"; -import Notifications from "../../vertical/header/Notification"; import Profile from "../../vertical/header/Profile"; import Search from "../../vertical/header/Search"; import Logo from "../../shared/logo/Logo"; @@ -85,7 +85,7 @@ export default function Header() { )} - + diff --git a/frontend/src/app/(app)/layout/vertical/header/About.tsx b/frontend/src/app/(app)/layout/vertical/header/About.tsx new file mode 100644 index 0000000..27ed723 --- /dev/null +++ b/frontend/src/app/(app)/layout/vertical/header/About.tsx @@ -0,0 +1,134 @@ +"use client"; + +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import IconButton from "@mui/material/IconButton"; +import Stack from "@mui/material/Stack"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import { Icon } from "@iconify/react"; +import { useEffect, useState } from "react"; + +interface ServerVersion { + version: string; + buildTime: string; + gitCommit: string; +} + +// The /api/v1/version probe predates the {success,data} envelope the rest of +// the API uses and returns bare snake_case JSON, so it is fetched directly +// rather than through the api client (which requires the envelope). Both +// shapes are accepted in case that endpoint is ever normalised. +async function fetchVersion(signal: AbortSignal): Promise { + const res = await fetch("/api/v1/version", { signal }); + if (!res.ok) throw new Error(`version request failed (${res.status})`); + const body = await res.json(); + const v = body?.data ?? body ?? {}; + return { + version: v.version ?? v.Version ?? "unknown", + buildTime: v.build_time ?? v.buildTime ?? "", + gitCommit: v.git_commit ?? v.gitCommit ?? "", + }; +} + +function formatBuildTime(raw: string): string { + if (!raw || raw === "unknown") return "—"; + const d = new Date(raw); + return Number.isNaN(d.getTime()) ? raw : d.toLocaleString(); +} + +// About shows the running server's build so an operator can confirm which +// version is deployed — and quote it verbatim in a bug report — without +// shelling onto the host. +export default function About() { + const [open, setOpen] = useState(false); + const [info, setInfo] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (!open) return; + const controller = new AbortController(); + setError(null); + fetchVersion(controller.signal) + .then(setInfo) + .catch((err) => { + if (err?.name !== "AbortError") setError("Could not read the server version."); + }); + return () => controller.abort(); + }, [open]); + + const summary = info + ? `OrchestrAD ${info.version}${info.gitCommit ? ` (${info.gitCommit})` : ""}` + : ""; + + const rows: Array<[string, string]> = info + ? [ + ["Version", info.version], + ["Built", formatBuildTime(info.buildTime)], + ["Commit", info.gitCommit || "—"], + ] + : []; + + return ( + <> + + setOpen(true)}> + + + + + setOpen(false)} fullWidth maxWidth="xs"> + About OrchestrAD + + + Active Directory rule automation. + + + {error && {error}} + + {!info && !error && ( + Reading server version… + )} + + {info && ( + + {rows.map(([label, value]) => ( + + + {label} + + + {value} + + + ))} + + )} + + + + API reference:{" "} + /api/docs + + + + + {info && ( + + )} + + + + + ); +} diff --git a/frontend/src/app/(app)/layout/vertical/header/AppLinks.tsx b/frontend/src/app/(app)/layout/vertical/header/AppLinks.tsx deleted file mode 100644 index c4014f4..0000000 --- a/frontend/src/app/(app)/layout/vertical/header/AppLinks.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import { Grid } from '@mui/material'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import * as dropdownData from './data'; -import Link from 'next/link'; -import React from 'react'; - -const AppLinks = () => { - return ( - ( - {dropdownData.appsLink.map((links, index) => ( - - - - - - - - - {links.title} - - - {links.subtext} - - - - - - ))} - ) - ); -}; - -export default AppLinks; diff --git a/frontend/src/app/(app)/layout/vertical/header/Header.tsx b/frontend/src/app/(app)/layout/vertical/header/Header.tsx index 4de1b34..fc884ad 100644 --- a/frontend/src/app/(app)/layout/vertical/header/Header.tsx +++ b/frontend/src/app/(app)/layout/vertical/header/Header.tsx @@ -8,8 +8,8 @@ import { styled } from '@mui/material/styles'; import config from '@/app/context/config' import { useContext } from "react"; import { Icon } from "@iconify/react"; +import About from "./About"; import ApiDocsLink from "./ApiDocsLink"; -import Notifications from "./Notification"; import Profile from "./Profile"; import Search from "./Search"; @@ -88,7 +88,7 @@ const Header = () => { - + diff --git a/frontend/src/app/(app)/layout/vertical/header/Notification.tsx b/frontend/src/app/(app)/layout/vertical/header/Notification.tsx deleted file mode 100644 index e95b4f2..0000000 --- a/frontend/src/app/(app)/layout/vertical/header/Notification.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useState } from "react"; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Chip from '@mui/material/Chip'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import Typography from '@mui/material/Typography'; -import * as dropdownData from "./data"; -import Scrollbar from "@/app/components/custom-scroll/Scrollbar"; - -import { Icon } from "@iconify/react"; -import { Stack } from "@mui/system"; -import Link from "next/link"; - -const Notifications = () => { - const [anchorEl2, setAnchorEl2] = useState(null); - - const handleClick2 = (event: React.MouseEvent) => { - setAnchorEl2(event.currentTarget); - }; - - const handleClose2 = () => { - setAnchorEl2(null); - }; - - return ( - - - {/* ------------------------------------------- */} - {/* Message Dropdown */} - {/* ------------------------------------------- */} - - - Notifications - - - - {dropdownData.notifications.map((notification, index) => ( - - - - - - - {notification.title} - - - {notification.subtitle} - - - - - - ))} - - - - - - - ); -}; - -export default Notifications; diff --git a/frontend/src/app/(app)/layout/vertical/header/Profile.tsx b/frontend/src/app/(app)/layout/vertical/header/Profile.tsx index 6805748..44fe683 100644 --- a/frontend/src/app/(app)/layout/vertical/header/Profile.tsx +++ b/frontend/src/app/(app)/layout/vertical/header/Profile.tsx @@ -45,7 +45,7 @@ const Profile = () => {