Docker images / prepare-release (push) Successful in 4s
Docker images / backend-image (push) Failing after 2m36s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 55s
Docker images / publish-release (push) Skipped
Подключить App Switcher, NavUser, OpsPanel и AlertDialog вместо Card-shell. Co-authored-by: Cursor <cursoragent@cursor.com>
163 lines
4.1 KiB
TypeScript
163 lines
4.1 KiB
TypeScript
import {
|
|
ChartBarIcon,
|
|
CloudIcon,
|
|
GlobeIcon,
|
|
LayoutDashboardIcon,
|
|
ServerIcon,
|
|
type LucideIcon,
|
|
} from "lucide-react"
|
|
import { formatAppVersionLabel, getAppVersion } from "@/lib/app-version"
|
|
|
|
export const CURRENT_APP_ID = "mm"
|
|
|
|
export type AppSwitcherIconName =
|
|
| "server"
|
|
| "cloud"
|
|
| "globe"
|
|
| "dashboard"
|
|
| "chart"
|
|
|
|
export type AppSwitcherEntry = {
|
|
id: string
|
|
name: string
|
|
subtitle?: string
|
|
url: string
|
|
icon: AppSwitcherIconName
|
|
shortcut?: string
|
|
enabled?: boolean
|
|
}
|
|
|
|
export type AppSwitcherConfig = {
|
|
menuLabel: string
|
|
apps: AppSwitcherEntry[]
|
|
}
|
|
|
|
export const APP_SWITCHER_ICONS: Record<AppSwitcherIconName, LucideIcon> = {
|
|
server: ServerIcon,
|
|
cloud: CloudIcon,
|
|
globe: GlobeIcon,
|
|
dashboard: LayoutDashboardIcon,
|
|
chart: ChartBarIcon,
|
|
}
|
|
|
|
function localAppUrl(): string {
|
|
if (typeof window !== "undefined") return window.location.origin
|
|
return process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, "") || "http://localhost:3000"
|
|
}
|
|
|
|
export function localMikrotikEntry(): AppSwitcherEntry {
|
|
return {
|
|
id: CURRENT_APP_ID,
|
|
name: "MikrotikManager",
|
|
subtitle: formatAppVersionLabel(getAppVersion()),
|
|
url: localAppUrl(),
|
|
icon: "dashboard",
|
|
enabled: true,
|
|
}
|
|
}
|
|
|
|
/** Offline fallback when auth-portal is unreachable. */
|
|
export const DEFAULT_APP_SWITCHER_CONFIG: AppSwitcherConfig = {
|
|
menuLabel: "Приложения",
|
|
apps: [
|
|
localMikrotikEntry(),
|
|
{
|
|
id: "cfdm",
|
|
name: "CF Domain Manager",
|
|
subtitle: "Управление доменами",
|
|
url: "https://cfdm.shnt.top",
|
|
icon: "cloud",
|
|
},
|
|
{
|
|
id: "vps",
|
|
name: "VPS Tracker",
|
|
subtitle: "Учёт виртуальных серверов",
|
|
url: "https://vps.shnt.top",
|
|
icon: "server",
|
|
},
|
|
{
|
|
id: "bgp",
|
|
name: "EvoBGP",
|
|
subtitle: "BGP маршрутизация",
|
|
url: "https://bgp.shnt.top",
|
|
icon: "globe",
|
|
},
|
|
{
|
|
id: "fw",
|
|
name: "EvoFirewall",
|
|
subtitle: "Firewall controller",
|
|
url: "https://fw.shnt.top",
|
|
icon: "server",
|
|
},
|
|
{
|
|
id: "dns",
|
|
name: "Technitium DNS",
|
|
subtitle: "DNS-сервер",
|
|
url: "https://dns.shnt.top",
|
|
icon: "globe",
|
|
},
|
|
],
|
|
}
|
|
|
|
const ICON_NAMES = new Set<AppSwitcherIconName>([
|
|
"server",
|
|
"cloud",
|
|
"globe",
|
|
"dashboard",
|
|
"chart",
|
|
])
|
|
|
|
function asIcon(value: unknown): AppSwitcherIconName {
|
|
return ICON_NAMES.has(value as AppSwitcherIconName)
|
|
? (value as AppSwitcherIconName)
|
|
: "server"
|
|
}
|
|
|
|
export function parseAppSwitcherConfig(raw: unknown): AppSwitcherConfig | null {
|
|
if (!raw || typeof raw !== "object") return null
|
|
const data = raw as { menuLabel?: unknown; apps?: unknown }
|
|
if (!Array.isArray(data.apps) || data.apps.length === 0) return null
|
|
|
|
const apps: AppSwitcherEntry[] = []
|
|
for (const item of data.apps) {
|
|
if (!item || typeof item !== "object") continue
|
|
const row = item as Record<string, unknown>
|
|
if (typeof row.id !== "string" || typeof row.name !== "string" || typeof row.url !== "string") {
|
|
continue
|
|
}
|
|
if (row.enabled === false) continue
|
|
apps.push({
|
|
id: row.id,
|
|
name: row.name,
|
|
subtitle: typeof row.subtitle === "string" ? row.subtitle : undefined,
|
|
url: row.url,
|
|
icon: asIcon(row.icon),
|
|
shortcut: typeof row.shortcut === "string" ? row.shortcut : undefined,
|
|
enabled: true,
|
|
})
|
|
}
|
|
if (apps.length === 0) return null
|
|
return {
|
|
menuLabel: typeof data.menuLabel === "string" && data.menuLabel ? data.menuLabel : "Приложения",
|
|
apps,
|
|
}
|
|
}
|
|
|
|
export function mergeWithLocalApp(config: AppSwitcherConfig): AppSwitcherConfig {
|
|
const local = localMikrotikEntry()
|
|
const rest = config.apps.filter((app) => app.id !== CURRENT_APP_ID)
|
|
return { ...config, apps: [local, ...rest] }
|
|
}
|
|
|
|
export function authPortalBaseUrl(): string | null {
|
|
const raw = process.env.NEXT_PUBLIC_AUTH_PORTAL_URL?.trim()
|
|
if (!raw) return null
|
|
return raw.replace(/\/$/, "")
|
|
}
|
|
|
|
export function getCurrentApp(
|
|
config: AppSwitcherConfig = DEFAULT_APP_SWITCHER_CONFIG,
|
|
): AppSwitcherEntry {
|
|
return config.apps.find((app) => app.id === CURRENT_APP_ID) ?? localMikrotikEntry()
|
|
}
|