Docker images / prepare-release (push) Successful in 9s
Docker images / backend-image (push) Successful in 1m43s
Docker images / frontend-image (push) Successful in 2m58s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 39s
Docker images / publish-release (push) Successful in 8s
Replaced existing KPI display implementations across multiple pages with the new KpiStatGrid component for a more consistent and visually appealing presentation of statistics. Updated the Backups, BGP, Certificates, Communities, Containers, Dashboard, and Data Collection pages to utilize the KpiStatGrid, improving the overall user experience and maintainability of the codebase. Additionally, added new dependencies in package.json for required libraries.
237 lines
5.6 KiB
TypeScript
237 lines
5.6 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react"
|
|
import { PanelLeftCloseIcon, ServerIcon } from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from "@/components/ui/sheet"
|
|
import {
|
|
ALL_SERVERS_ID,
|
|
ServerTileRail,
|
|
hostnameOf,
|
|
type ServerTileItem,
|
|
} from "@/components/server-tile-rail"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
/** Preview: https://reui.io/preview/base/app-shell-12 · https://reui.io/preview/base/list-9 */
|
|
|
|
const STORAGE_KEY = "mm:server-rail-collapsed"
|
|
const DESKTOP_MQ = "(min-width: 768px)"
|
|
|
|
function readCollapsed(): boolean {
|
|
try {
|
|
return window.localStorage.getItem(STORAGE_KEY) === "1"
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function writeCollapsed(value: boolean) {
|
|
try {
|
|
window.localStorage.setItem(STORAGE_KEY, value ? "1" : "0")
|
|
} catch {
|
|
// quota / private mode
|
|
}
|
|
}
|
|
|
|
interface ServerRailContextValue {
|
|
openRail: () => void
|
|
expandRail: () => void
|
|
collapsed: boolean
|
|
selectedLabel: string
|
|
}
|
|
|
|
const ServerRailContext = createContext<ServerRailContextValue | null>(null)
|
|
|
|
function useServerRail() {
|
|
const ctx = useContext(ServerRailContext)
|
|
if (!ctx) {
|
|
throw new Error("ServerRailMobileButton must be used inside ServerRailLayout")
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
function ServerRailMobileButton() {
|
|
const { openRail, expandRail, collapsed, selectedLabel } = useServerRail()
|
|
return (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className={collapsed ? undefined : "md:hidden"}
|
|
aria-label={collapsed ? "Показать список серверов" : "Серверы"}
|
|
onClick={() => {
|
|
if (window.matchMedia(DESKTOP_MQ).matches) {
|
|
expandRail()
|
|
return
|
|
}
|
|
openRail()
|
|
}}
|
|
>
|
|
<ServerIcon data-icon="inline-start" />
|
|
{selectedLabel}
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
function ServerRailLayout({
|
|
items,
|
|
selectedId,
|
|
onSelect,
|
|
showAll = true,
|
|
allCount = 0,
|
|
showCount = true,
|
|
showType = true,
|
|
headerRight,
|
|
loading = false,
|
|
extra,
|
|
header,
|
|
banner,
|
|
contentClassName,
|
|
children,
|
|
}: {
|
|
items: ServerTileItem[]
|
|
selectedId: string
|
|
onSelect: (id: string) => void
|
|
showAll?: boolean
|
|
allCount?: number
|
|
showCount?: boolean
|
|
showType?: boolean
|
|
headerRight?: ReactNode
|
|
loading?: boolean
|
|
extra?: ReactNode
|
|
header?: ReactNode
|
|
banner?: ReactNode
|
|
contentClassName?: string
|
|
children: ReactNode
|
|
}) {
|
|
const [railOpen, setRailOpen] = useState(false)
|
|
const [collapsed, setCollapsed] = useState(false)
|
|
const [hydrated, setHydrated] = useState(false)
|
|
|
|
useEffect(() => {
|
|
setCollapsed(readCollapsed())
|
|
setHydrated(true)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (!hydrated) return
|
|
writeCollapsed(collapsed)
|
|
}, [collapsed, hydrated])
|
|
|
|
const handleSelect = useCallback(
|
|
(id: string) => {
|
|
onSelect(id)
|
|
setRailOpen(false)
|
|
},
|
|
[onSelect],
|
|
)
|
|
|
|
const expandRail = useCallback(() => {
|
|
setCollapsed(false)
|
|
}, [])
|
|
|
|
const selectedLabel = useMemo(() => {
|
|
if (showAll && selectedId === ALL_SERVERS_ID) return "Все серверы"
|
|
const item = items.find((entry) => entry.id === selectedId)
|
|
return item ? hostnameOf(item) : "Сервер"
|
|
}, [items, selectedId, showAll])
|
|
|
|
const ctx = useMemo<ServerRailContextValue>(
|
|
() => ({
|
|
openRail: () => setRailOpen(true),
|
|
expandRail,
|
|
collapsed,
|
|
selectedLabel,
|
|
}),
|
|
[collapsed, expandRail, selectedLabel],
|
|
)
|
|
|
|
const railHeaderRight = (
|
|
<>
|
|
{headerRight}
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
className="hidden md:inline-flex"
|
|
aria-label="Свернуть список серверов"
|
|
onClick={() => setCollapsed(true)}
|
|
>
|
|
<PanelLeftCloseIcon />
|
|
</Button>
|
|
</>
|
|
)
|
|
|
|
const rail = (
|
|
<ServerTileRail
|
|
items={items}
|
|
selectedId={selectedId}
|
|
onSelect={handleSelect}
|
|
showAll={showAll}
|
|
allCount={allCount}
|
|
showCount={showCount}
|
|
showType={showType}
|
|
headerRight={railHeaderRight}
|
|
loading={loading}
|
|
className="min-h-0 flex-1"
|
|
/>
|
|
)
|
|
|
|
return (
|
|
<ServerRailContext.Provider value={ctx}>
|
|
<div className="flex h-full min-h-0 flex-col">
|
|
{header}
|
|
{banner}
|
|
<div className="flex min-h-0 flex-1">
|
|
{!collapsed ? (
|
|
<aside className="hidden min-h-0 w-60 shrink-0 flex-col gap-3 p-3 pr-0 md:flex">
|
|
{rail}
|
|
{extra}
|
|
</aside>
|
|
) : null}
|
|
<div className={cn("min-w-0 flex-1 overflow-y-auto p-4 md:p-6", contentClassName)}>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
<Sheet open={railOpen} onOpenChange={setRailOpen}>
|
|
<SheetContent side="left" className="flex w-72 flex-col gap-3 p-3" showCloseButton>
|
|
<SheetHeader className="px-1 pt-1">
|
|
<SheetTitle>Серверы</SheetTitle>
|
|
</SheetHeader>
|
|
<div className="flex min-h-0 flex-1 flex-col gap-3">
|
|
<ServerTileRail
|
|
items={items}
|
|
selectedId={selectedId}
|
|
onSelect={handleSelect}
|
|
showAll={showAll}
|
|
allCount={allCount}
|
|
showCount={showCount}
|
|
showType={showType}
|
|
showHeader={false}
|
|
headerRight={headerRight}
|
|
loading={loading}
|
|
className="min-h-0 flex-1"
|
|
/>
|
|
{extra}
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</div>
|
|
</ServerRailContext.Provider>
|
|
)
|
|
}
|
|
|
|
export { ServerRailLayout, ServerRailMobileButton }
|