Compare commits

...
4 Commits
Author SHA1 Message Date
Denozordec 1a142e68a9 refactor: update Dashboard components for improved layout and integration
CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 52s
CI / go (push) Successful in 59s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 4m20s
Refactored the DashboardQuickLinkCard to enhance its structure and presentation by integrating FramePanel. Updated the DashboardQuickLinks component to utilize Frame for better organization, including a header and description. Removed the previous DashboardFramePanel integration in the DashboardComponent to streamline the layout, ensuring a more cohesive user experience.
2026-07-09 21:15:04 +07:00
Denozordec 46e7b2aa6b chore: update BadgeTabs component to log additional layout metrics
CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 49s
CI / go (push) Successful in 1m0s
CI / bird2 (push) Successful in 17s
CI / release (push) Successful in 22s
Added new layout metrics logging to the BadgeTabs component, enhancing debugging capabilities. This update includes additional timestamped entries for layout metrics, providing better insights into component behavior during runtime.
2026-07-09 21:06:33 +07:00
Denozordec 1152bb40f7 feat: enhance BadgeTabs and Auth components with layout improvements and API integration
CI / changes (push) Successful in 13s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m7s
CI / go (push) Successful in 1m14s
CI / bird2 (push) Successful in 20s
CI / release (push) Successful in 4m52s
Added layout metrics logging to the BadgeTabs component for better debugging and analysis. Updated the Auth route to include a reason for redirection when a token is required, improving user feedback. Enhanced the Settings component to display an alert when API token access is needed, ensuring clarity for users. Additionally, refined the Tabs component to improve responsiveness based on orientation, contributing to a more cohesive user experience.
2026-07-09 20:54:35 +07:00
Denozordec 538daea0f1 refactor: update dashboard components for improved layout and functionality
CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m7s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m21s
Refactored the DashboardQuickLinks component to utilize the new DashboardQuickLinkCard for better organization and presentation. Enhanced the DashboardComponent to integrate quick links within a DashboardFramePanel, improving the overall user experience. Updated the metric-tile component import path for consistency. Removed unnecessary imports from the separator component to streamline the codebase.
2026-07-09 18:49:55 +07:00
16 changed files with 436 additions and 42 deletions
+16
View File
@@ -0,0 +1,16 @@
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605101206}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605101204}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605138640}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605138642}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605143758}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605144457}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605145907}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605152669}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605152671}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605222064}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605222067}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605819320}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605827145}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605864391}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783606471768}
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783606472923}
@@ -0,0 +1,99 @@
"use client"
import { useEffect, useRef } from "react"
import { cn } from "@evobgp/ui/lib/utils"
// Deterministic per-dot value so the field reads as noise, not a flat grid.
function grain(i: number, j: number) {
const n = Math.sin(i * 127.1 + j * 311.7) * 43758.5453
return n - Math.floor(n)
}
/**
* Static dot field adapted from auth-3's WaveDots backdrop with the twinkle
* animation removed: a dense grid of dots at a fixed per-dot brightness,
* painted once and again on resize or theme change. The dot color resolves
* from the canvas `color` token so it stays neutral in light and dark.
* customize: GAP (density), DOT (size), PEAK (max brightness).
*/
export function CardDotField({ className }: { className?: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
const GAP = 3 // dot spacing in px (tight grid, still distinct dots)
const DOT = 1.5 // dot side in px
const BASE = 0.05 // dim end of a dot
const PEAK = 0.32 // bright end of a dot (kept subtle for a card surface)
const draw = () => {
const rect = canvas.getBoundingClientRect()
if (!rect.width || !rect.height) return
// Resetting width clears the canvas and restores the identity transform,
// so the color probe below reads a raw device pixel.
const dpr = Math.min(window.devicePixelRatio || 1, 2)
canvas.width = Math.round(rect.width * dpr)
canvas.height = Math.round(rect.height * dpr)
// Resolve the token to concrete sRGB by painting + reading it back, so
// oklch never casts a color and dark mode recolors on theme change.
ctx.fillStyle = getComputedStyle(canvas).color || "rgb(115,115,115)"
ctx.fillRect(0, 0, 1, 1)
const px = ctx.getImageData(0, 0, 1, 1).data
const color = `rgb(${px[0]}, ${px[1]}, ${px[2]})`
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
ctx.clearRect(0, 0, rect.width, rect.height)
ctx.fillStyle = color
const cols = Math.ceil(rect.width / GAP) + 1
const rows = Math.ceil(rect.height / GAP) + 1
for (let i = 0; i < cols; i++) {
const x = i * GAP
for (let j = 0; j < rows; j++) {
const q = grain(i, j)
const amp = 0.7 + 0.6 * grain(j * 2 + 1, i * 2 + 1)
let a = (BASE + (PEAK - BASE) * q * q) * amp
if (a > 1) a = 1
ctx.globalAlpha = a
ctx.fillRect(x, j * GAP, DOT, DOT)
}
}
ctx.globalAlpha = 1
}
draw()
const resizeObserver = new ResizeObserver(() => draw())
resizeObserver.observe(canvas)
// Repaint when the theme class toggles so the resolved color stays correct.
const themeObserver = new MutationObserver(() => draw())
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "style"],
})
return () => {
resizeObserver.disconnect()
themeObserver.disconnect()
}
}, [])
return (
<canvas
ref={canvasRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full",
className
)}
/>
)
}
@@ -0,0 +1,17 @@
import { Frame } from "@/components/reui/frame"
import { CardItem } from "./card-item"
import { CARDS } from "./data"
export function CardGrid() {
return (
<Frame className="@container w-full">
{/* Grid */}
<div className="grid gap-1 @2xl:grid-cols-2">
{CARDS.map((card) => (
<CardItem key={card.title} card={card} />
))}
</div>
</Frame>
)
}
@@ -0,0 +1,44 @@
import { FramePanel } from "@/components/reui/frame"
import { cn } from "@evobgp/ui/lib/utils"
import { Item, ItemMedia } from "@evobgp/ui/components/item"
import { CardDotField } from "./card-dot-field"
import { ICard } from "./data"
import { ChevronRightIcon } from "lucide-react"
export function CardItem({ card }: { card: ICard }) {
return (
<FramePanel className="isolate">
{/* Card */}
<CardDotField className="text-muted-foreground [mask-image:linear-gradient(to_bottom_left,black,transparent_60%)]" />
<div className="relative z-10 space-y-7.5">
<Item
className={cn(
"p-0",
"border-background flex size-11 items-center justify-center border-2 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-5 [&_svg]:text-white",
card.iconBg
)}
>
<ItemMedia variant="icon" className="size-auto">
{card.icon}
</ItemMedia>
</Item>
<div className="flex flex-col gap-3">
<span className="block text-sm leading-tight font-medium">
{card.title}
</span>
<p className="text-muted-foreground text-xs leading-relaxed">
{card.description}
</p>
<a
href="#"
className="text-primay inline-flex items-center gap-1 text-xs underline-offset-2 hover:underline"
>
{card.link}
<ChevronRightIcon aria-hidden="true" className="size-2.5 shrink-0" />
</a>
</div>
</div>
</FramePanel>
)
}
@@ -0,0 +1,53 @@
import { type ReactNode } from "react"
import { ShoppingBagIcon, TrendingUp, BarChart3Icon, Settings2Icon } from "lucide-react"
export interface ICard {
title: string
description: string
link: string
icon: ReactNode
iconBg: string
}
export const CARDS: ICard[] = [
{
title: "Recent Orders Overview",
description:
"Track and review all recent purchases, updates, and status changes in one place.",
link: "View Orders",
icon: (
<ShoppingBagIcon aria-hidden="true" />
),
iconBg: "bg-green-600",
},
{
title: "Active Opportunities Pipeline",
description:
"Monitor ongoing deals, check potential revenue, and update opportunity stages.",
link: "Open Pipeline",
icon: (
<TrendingUp aria-hidden="true" />
),
iconBg: "bg-indigo-600",
},
{
title: "Performance & Sales Reports",
description:
"Analyze weekly and monthly reports to gain deeper insights into performance trends.",
link: "View Reports",
icon: (
<BarChart3Icon aria-hidden="true" />
),
iconBg: "bg-sky-600",
},
{
title: "Integration Settings & Sync",
description:
"Manage connections with third-party tools and ensure data stays in sync.",
link: "Manage Integrations",
icon: (
<Settings2Icon aria-hidden="true" />
),
iconBg: "bg-orange-600",
},
]
@@ -0,0 +1,9 @@
import { CardGrid } from "./components/card-grid"
export function Page() {
return (
<div className="flex min-h-svh w-full max-w-4xl items-center justify-center p-8">
<CardGrid />
</div>
)
}
@@ -2,7 +2,7 @@ import { Badge } from "@/components/reui/badge"
import { Card, CardContent } from "@evobgp/ui/components/card"
import { CardDotField } from "./card-dot-field"
import { CardDotField } from "@/components/dashboard/card-dot-field"
import { type MetricCard } from "./data"
import { PanelCorners } from "./panel-heading"
import { toneStyles } from "./tone-styles"
@@ -0,0 +1,87 @@
'use client'
import { useEffect, useRef } from 'react'
import { cn } from '@evobgp/ui/lib/utils'
function grain(i: number, j: number) {
const n = Math.sin(i * 127.1 + j * 311.7) * 43758.5453
return n - Math.floor(n)
}
/**
* Static dot field adapted from REUI card-17: theme-aware canvas dots,
* repainted on resize and theme change.
*/
export function CardDotField({ className }: { className?: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const GAP = 3
const DOT = 1.5
const BASE = 0.05
const PEAK = 0.32
const draw = () => {
const rect = canvas.getBoundingClientRect()
if (!rect.width || !rect.height) return
const dpr = Math.min(window.devicePixelRatio || 1, 2)
canvas.width = Math.round(rect.width * dpr)
canvas.height = Math.round(rect.height * dpr)
ctx.fillStyle = getComputedStyle(canvas).color || 'rgb(115,115,115)'
ctx.fillRect(0, 0, 1, 1)
const px = ctx.getImageData(0, 0, 1, 1).data
const color = `rgb(${px[0]}, ${px[1]}, ${px[2]})`
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
ctx.clearRect(0, 0, rect.width, rect.height)
ctx.fillStyle = color
const cols = Math.ceil(rect.width / GAP) + 1
const rows = Math.ceil(rect.height / GAP) + 1
for (let i = 0; i < cols; i++) {
const x = i * GAP
for (let j = 0; j < rows; j++) {
const q = grain(i, j)
const amp = 0.7 + 0.6 * grain(j * 2 + 1, i * 2 + 1)
let a = (BASE + (PEAK - BASE) * q * q) * amp
if (a > 1) a = 1
ctx.globalAlpha = a
ctx.fillRect(x, j * GAP, DOT, DOT)
}
}
ctx.globalAlpha = 1
}
draw()
const resizeObserver = new ResizeObserver(() => draw())
resizeObserver.observe(canvas)
const themeObserver = new MutationObserver(() => draw())
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'style'],
})
return () => {
resizeObserver.disconnect()
themeObserver.disconnect()
}
}, [])
return (
<canvas
ref={canvasRef}
aria-hidden="true"
className={cn('pointer-events-none absolute inset-0 h-full w-full', className)}
/>
)
}
@@ -0,0 +1,68 @@
import { Link } from '@tanstack/react-router'
import { ChevronRight } from 'lucide-react'
import type { ReactNode } from 'react'
import { CardDotField } from '@/components/dashboard/card-dot-field'
import { FramePanel } from '@/components/reui/frame'
import { cn } from '@evobgp/ui/lib/utils'
import { Item, ItemMedia } from '@evobgp/ui/components/item'
export type QuickLinkCardProps = {
icon: ReactNode
label: string
description: string
to: string
search?: Record<string, string>
iconClass: string
}
export function DashboardQuickLinkCard({
icon,
label,
description,
to,
search,
iconClass,
}: QuickLinkCardProps) {
return (
<Link
to={to}
search={search}
className="group block h-full rounded-[inherit] focus-visible:outline-none"
aria-label={`${label}: ${description}`}
>
<FramePanel
className={cn(
'relative isolate h-full overflow-hidden transition-colors',
'hover:border-foreground/20',
'group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-background',
)}
>
<CardDotField className="text-muted-foreground [mask-image:linear-gradient(to_bottom_left,black,transparent_60%)]" />
<div className="relative z-10 flex h-full flex-col gap-7.5">
<Item
className={cn(
'border-background flex size-11 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-5',
iconClass,
)}
>
<ItemMedia variant="icon" className="size-auto">
{icon}
</ItemMedia>
</Item>
<div className="mt-auto flex flex-col gap-3">
<span className="text-foreground block text-sm leading-tight font-medium">{label}</span>
<p className="text-muted-foreground text-xs leading-relaxed">{description}</p>
<span className="text-primary inline-flex items-center gap-1 text-xs font-medium underline-offset-2 group-hover:underline">
Перейти
<ChevronRight
aria-hidden
className="size-2.5 shrink-0 transition-transform group-hover:translate-x-0.5"
/>
</span>
</div>
</div>
</FramePanel>
</Link>
)
}
@@ -1,11 +1,12 @@
import { Link } from '@tanstack/react-router'
import { Gauge, Network, Play, Plus, Share2, Tags } from 'lucide-react'
import type { ReactNode } from 'react'
import { DashboardQuickLinkCard } from '@/components/dashboard/dashboard-quick-link-card'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
type QuickLink = {
@@ -14,6 +15,7 @@ type QuickLink = {
description: string
to: string
search?: Record<string, string>
iconClass: string
}
const LINKS: QuickLink[] = [
@@ -22,12 +24,14 @@ const LINKS: QuickLink[] = [
label: 'Создать модуль',
description: 'Новый модуль маршрутизации и источники префиксов.',
to: '/modules/new',
iconClass: 'bg-primary text-primary-foreground [&_svg]:text-primary-foreground',
},
{
icon: <Tags aria-hidden />,
label: 'BGP-сообщества',
description: 'Справочник communities для политик экспорта.',
to: '/directories',
iconClass: 'bg-info text-info-foreground [&_svg]:text-info-foreground',
},
{
icon: <Network aria-hidden />,
@@ -35,6 +39,7 @@ const LINKS: QuickLink[] = [
description: 'Обзор пиров, спикеров и live-сессий BGP.',
to: '/network',
search: { tab: 'overview' },
iconClass: 'bg-success text-success-foreground [&_svg]:text-success-foreground',
},
{
icon: <Share2 aria-hidden />,
@@ -42,6 +47,7 @@ const LINKS: QuickLink[] = [
description: 'Настройка BGP-соседа и шаблонов сессии.',
to: '/network',
search: { tab: 'peers' },
iconClass: 'bg-warning text-warning-foreground [&_svg]:text-warning-foreground',
},
{
icon: <Play aria-hidden />,
@@ -49,6 +55,7 @@ const LINKS: QuickLink[] = [
description: 'Ревизии конфигурации и применение на нодах.',
to: '/operations',
search: { tab: 'revisions' },
iconClass: 'bg-focus text-focus-foreground [&_svg]:text-focus-foreground',
},
{
icon: <Gauge aria-hidden />,
@@ -56,32 +63,22 @@ const LINKS: QuickLink[] = [
description: 'Состояние системы, BIRD и PostgreSQL.',
to: '/monitoring',
search: { tab: 'system' },
iconClass: 'bg-destructive text-destructive-foreground [&_svg]:text-destructive-foreground',
},
]
export function DashboardQuickLinks() {
return (
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{LINKS.map((link) => (
<Frame key={link.label} spacing="sm">
<FrameHeader className="px-1! py-1!">
<div className="[&_svg]:text-muted-foreground flex items-center gap-2 [&_svg]:size-4">
{link.icon}
<span className="text-foreground text-sm font-medium">{link.label}</span>
</div>
</FrameHeader>
<FramePanel className="space-y-3.5">
<p className="text-muted-foreground text-xs leading-relaxed">{link.description}</p>
<Link
to={link.to}
search={link.search}
className="text-primary inline-flex items-center gap-1 text-xs font-medium underline-offset-2 hover:underline"
>
Перейти
</Link>
</FramePanel>
</Frame>
))}
</div>
<Frame className="@container w-full">
<FrameHeader>
<FrameTitle>Быстрые действия</FrameTitle>
<FrameDescription>Частые переходы к настройке и деплою</FrameDescription>
</FrameHeader>
<div className="grid gap-1 sm:grid-cols-2 xl:grid-cols-3">
{LINKS.map((link) => (
<DashboardQuickLinkCard key={link.label} {...link} />
))}
</div>
</Frame>
)
}
+1 -1
View File
@@ -9,7 +9,7 @@ export const Route = createFileRoute('/_auth')({
const raw =
typeof window !== 'undefined' ? window.localStorage.getItem(TOKEN_STORAGE_KEY) : null
if (!raw || !normalizeApiToken(raw)) {
throw redirect({ to: '/settings' })
throw redirect({ to: '/settings', search: { reason: 'token-required' } })
}
},
component: AuthLayout,
+2 -8
View File
@@ -85,6 +85,8 @@ function DashboardComponent() {
}
/>
<DashboardQuickLinks />
{initialLoading ? (
<AnalyticsDashboardSkeleton />
) : (
@@ -142,14 +144,6 @@ function DashboardComponent() {
)}
</DashboardFramePanel>
</div>
<section className="space-y-3">
<div className="space-y-0.5">
<h2 className="text-sm font-semibold">Быстрые действия</h2>
<p className="text-muted-foreground text-sm">Частые переходы к настройке и деплою</p>
</div>
<DashboardQuickLinks />
</section>
</div>
)
}
+13 -1
View File
@@ -1,7 +1,8 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { createFileRoute, useNavigate, useRouterState } from '@tanstack/react-router'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Button } from '@evobgp/ui/components/button'
import { Alert, AlertDescription } from '@evobgp/ui/components/alert'
import { PanelCard } from '@/components/panel-card'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
@@ -28,6 +29,9 @@ const THEME_SELECT_ITEMS = [
function SettingsComponent() {
const navigate = useNavigate()
const tokenRequired = useRouterState({
select: (s) => new URLSearchParams(s.location.search).get('reason') === 'token-required',
})
const qc = useQueryClient()
const { data: session, isError: sessionError, error: sessionQueryError } = useQuery({
...authSessionQueryOptions(),
@@ -69,6 +73,14 @@ function SettingsComponent() {
description="Параметры интерфейса и подключения браузера к API."
/>
{tokenRequired ? (
<Alert>
<AlertDescription>
Для доступа к разделу нужен API-токен. Сохраните токен ниже или нажмите «Использовать dev».
</AlertDescription>
</Alert>
) : null}
<PanelCard
title="Подключение к API"
description="Токен хранится только в этом браузере (localStorage). Управление ключами tenant — в разделе «Права доступа»."
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,5 +1,3 @@
"use client"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "@evobgp/ui/lib/utils"
+4 -4
View File
@@ -15,7 +15,7 @@ function Tabs({
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className
)}
{...props}
@@ -24,7 +24,7 @@ function Tabs({
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-8 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: {
@@ -58,10 +58,10 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
)}
{...props}