feat(web): move version display to the config page header (#2273)

- remove version details from the sidebar footer
- show the current app version as a badge in the config page header
- add a reusable Badge UI component for the new version label
This commit is contained in:
wenjie 2026-04-02 19:09:27 +08:00 committed by GitHub
parent bae4342af1
commit e075be6b10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 69 additions and 30 deletions

View file

@ -11,12 +11,10 @@ import {
IconSparkles, IconSparkles,
IconTools, IconTools,
} from "@tabler/icons-react" } from "@tabler/icons-react"
import { useQuery } from "@tanstack/react-query"
import { Link, useRouterState } from "@tanstack/react-router" import { Link, useRouterState } from "@tanstack/react-router"
import * as React from "react" import * as React from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { getSystemVersionInfo } from "@/api/system"
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
@ -25,7 +23,6 @@ import {
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
SidebarFooter,
SidebarGroup, SidebarGroup,
SidebarGroupContent, SidebarGroupContent,
SidebarGroupLabel, SidebarGroupLabel,
@ -84,13 +81,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
language: (i18n.resolvedLanguage ?? i18n.language ?? "").toLowerCase(), language: (i18n.resolvedLanguage ?? i18n.language ?? "").toLowerCase(),
t, t,
}) })
const { data: versionInfo } = useQuery({
queryKey: ["system", "version"],
queryFn: getSystemVersionInfo,
staleTime: 5 * 60 * 1000,
})
const versionText = versionInfo?.version ?? t("footer.version_unknown")
const handleNavItemClick = React.useCallback(() => { const handleNavItemClick = React.useCallback(() => {
if (isMobile) { if (isMobile) {
setOpenMobile(false) setOpenMobile(false)
@ -263,26 +254,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</Collapsible> </Collapsible>
))} ))}
</SidebarContent> </SidebarContent>
<SidebarFooter className="border-t-border/30 border-t px-3 py-2 group-data-[collapsible=icon]:hidden">
<div className="text-muted-foreground flex flex-col gap-0.5 text-[11px] leading-4">
<div className="truncate" title={versionText}>
<span className="text-foreground/80">{t("footer.version")}:</span>{" "}
{versionText}
</div>
{versionInfo?.git_commit && (
<div className="truncate" title={versionInfo.git_commit}>
<span className="text-foreground/80">{t("footer.commit")}:</span>{" "}
{versionInfo.git_commit}
</div>
)}
{versionInfo?.build_time && (
<div className="truncate" title={versionInfo.build_time}>
<span className="text-foreground/80">{t("footer.build")}:</span>{" "}
{versionInfo.build_time}
</div>
)}
</div>
</SidebarFooter>
<SidebarRail /> <SidebarRail />
</Sidebar> </Sidebar>
) )

View file

@ -1,4 +1,4 @@
import { IconCode, IconDeviceFloppy } from "@tabler/icons-react" import { IconCode, IconDeviceFloppy, IconTag } from "@tabler/icons-react"
import { useQuery, useQueryClient } from "@tanstack/react-query" import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Link } from "@tanstack/react-router" import { Link } from "@tanstack/react-router"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
@ -10,6 +10,7 @@ import { launcherFetch } from "@/api/http"
import { import {
getAutoStartStatus, getAutoStartStatus,
getLauncherConfig, getLauncherConfig,
getSystemVersionInfo,
setAutoStartEnabled as updateAutoStartEnabled, setAutoStartEnabled as updateAutoStartEnabled,
setLauncherConfig as updateLauncherConfig, setLauncherConfig as updateLauncherConfig,
} from "@/api/system" } from "@/api/system"
@ -32,6 +33,7 @@ import {
parseMultilineList, parseMultilineList,
} from "@/components/config/form-model" } from "@/components/config/form-model"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { refreshGatewayState } from "@/store/gateway" import { refreshGatewayState } from "@/store/gateway"
@ -64,6 +66,12 @@ export function ConfigPage() {
queryFn: getLauncherConfig, queryFn: getLauncherConfig,
}) })
const { data: versionInfo } = useQuery({
queryKey: ["system", "version"],
queryFn: getSystemVersionInfo,
staleTime: 5 * 60 * 1000,
})
const { const {
data: autoStartStatus, data: autoStartStatus,
isLoading: isAutoStartLoading, isLoading: isAutoStartLoading,
@ -297,6 +305,17 @@ export function ConfigPage() {
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<PageHeader <PageHeader
title={t("navigation.config")} title={t("navigation.config")}
titleExtra={
versionInfo && (
<Badge
variant="secondary"
className="gap-1 font-mono text-[11px] font-normal opacity-80"
>
<IconTag className="size-3 opacity-70" />
{versionInfo.version}
</Badge>
)
}
children={ children={
<Button variant="outline" asChild> <Button variant="outline" asChild>
<Link to="/config/raw"> <Link to="/config/raw">

View file

@ -0,0 +1,49 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }