refactor(web): streamline component structure by removing unused refs and logging
Build, Test, and Push CFDM Docker Image / test (push) Successful in 4m34s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m46s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 5s
Build, Test, and Push CFDM Docker Image / test (push) Successful in 4m34s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m46s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 5s
- Removed unnecessary useRef and useEffect hooks from PageShell, OpsDashboard, ResourcePage, SettingsShell, and other components to simplify code and improve maintainability. - Updated components to utilize direct className assignments for layout without relying on refs for width logging. - Introduced CountedLineTabs in various components to enhance tab functionality and improve user experience. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { Tabs, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
|
||||
export interface CountedLineTab {
|
||||
id: string
|
||||
label: string
|
||||
count?: number
|
||||
}
|
||||
|
||||
interface CountedLineTabsProps {
|
||||
tabs: CountedLineTab[]
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
className?: string
|
||||
listClassName?: string
|
||||
}
|
||||
|
||||
/** Line tabs with count pills (c-tabs-2 / data-grid-filtering-2). */
|
||||
export function CountedLineTabs({
|
||||
tabs,
|
||||
value,
|
||||
onValueChange,
|
||||
className,
|
||||
listClassName,
|
||||
}: CountedLineTabsProps) {
|
||||
return (
|
||||
<Tabs value={value} onValueChange={onValueChange} className={className}>
|
||||
<TabsList variant="line" className={cn('gap-5', listClassName)}>
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
||||
>
|
||||
<span>{tab.label}</span>
|
||||
{tab.count !== undefined ? (
|
||||
<span className="bg-muted text-muted-foreground inline-flex min-w-5 items-center justify-center rounded-md px-1.5 py-0.5 text-xs tabular-nums">
|
||||
{tab.count}
|
||||
</span>
|
||||
) : null}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useEffect, useRef, type ReactNode } from 'react'
|
||||
import { useRouterState } from '@tanstack/react-router'
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
import { DEBUG_BUILD_STAMP, debugAgentLog } from '@/lib/debug-agent-log'
|
||||
|
||||
interface PageShellProps {
|
||||
children: ReactNode
|
||||
@@ -9,37 +7,8 @@ interface PageShellProps {
|
||||
}
|
||||
|
||||
export function PageShell({ children, className }: PageShellProps) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
const main = el.closest('main')
|
||||
const mainWidth = main?.clientWidth ?? 0
|
||||
const shellWidth = el.clientWidth
|
||||
const child = el.firstElementChild as HTMLElement | null
|
||||
const childWidth = child?.clientWidth ?? 0
|
||||
const childMaxWidth = child ? getComputedStyle(child).maxWidth : 'none'
|
||||
debugAgentLog(
|
||||
'page-shell.tsx:mount',
|
||||
'page layout widths',
|
||||
{
|
||||
buildStamp: DEBUG_BUILD_STAMP,
|
||||
pathname,
|
||||
mainWidth,
|
||||
shellWidth,
|
||||
childWidth,
|
||||
childMaxWidth,
|
||||
shellClass: el.className,
|
||||
childClass: child?.className ?? null,
|
||||
},
|
||||
'A',
|
||||
)
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn('flex flex-col gap-4 md:gap-6', className)}>
|
||||
<div className={cn('flex flex-col gap-4 md:gap-6', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, type ReactNode } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
Frame,
|
||||
FrameDescription,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
FrameTitle,
|
||||
} from '@/components/reui/frame'
|
||||
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
||||
import { debugAgentLog } from '@/lib/debug-agent-log'
|
||||
import { KpiStatGrid, type KpiStatCard } from './kpi-stat-grid'
|
||||
|
||||
export type OpsKpiCard = KpiStatCard
|
||||
@@ -21,9 +20,12 @@ interface OpsDashboardProps {
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const rootClassName =
|
||||
'text-foreground @container flex w-full flex-col gap-2 md:gap-3'
|
||||
|
||||
function OpsDashboardSkeleton() {
|
||||
return (
|
||||
<div className="text-foreground @container mx-auto flex w-full max-w-7xl flex-col gap-2 md:gap-3">
|
||||
<div className={rootClassName}>
|
||||
<header className="px-1">
|
||||
<Skeleton className="h-7 w-56" />
|
||||
<Skeleton className="mt-2 h-4 w-80 max-w-full" />
|
||||
@@ -46,33 +48,12 @@ export function OpsDashboard({
|
||||
queue,
|
||||
isLoading = false,
|
||||
}: OpsDashboardProps) {
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return
|
||||
const el = rootRef.current
|
||||
if (!el) return
|
||||
debugAgentLog(
|
||||
'ops-dashboard.tsx:mount',
|
||||
'ops dashboard width',
|
||||
{
|
||||
clientWidth: el.clientWidth,
|
||||
maxWidth: getComputedStyle(el).maxWidth,
|
||||
className: el.className,
|
||||
},
|
||||
'A',
|
||||
)
|
||||
}, [isLoading])
|
||||
|
||||
if (isLoading) {
|
||||
return <OpsDashboardSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className="text-foreground @container mx-auto flex w-full max-w-7xl flex-col gap-2 md:gap-3"
|
||||
>
|
||||
<div className={rootClassName}>
|
||||
<header className="px-1">
|
||||
<div className="flex flex-col gap-px">
|
||||
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState, useEffect, useRef, type ReactNode } from 'react'
|
||||
import { useCallback, useMemo, useState, type ReactNode } from 'react'
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@tanstack/react-table'
|
||||
import { FilterIcon, FunnelXIcon } from 'lucide-react'
|
||||
|
||||
import { CountedLineTabs } from '@/components/counted-line-tabs'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { DataGrid } from '@/components/reui/data-grid/data-grid'
|
||||
import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination'
|
||||
@@ -32,11 +33,9 @@ import {
|
||||
import { Button } from '@cfdm/ui/components/button'
|
||||
import { Separator } from '@cfdm/ui/components/separator'
|
||||
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@cfdm/ui/components/alert'
|
||||
import { EmptyState } from '@/components/empty-state'
|
||||
import { applyFiltersToData } from './filter-utils'
|
||||
import { debugAgentLog } from '@/lib/debug-agent-log'
|
||||
|
||||
export interface ResourcePageTab {
|
||||
id: string
|
||||
@@ -122,7 +121,6 @@ export function ResourcePage<T extends object>({
|
||||
toolbarExtra,
|
||||
hideHeader = false,
|
||||
}: ResourcePageProps<T>) {
|
||||
const frameRef = useRef<HTMLDivElement>(null)
|
||||
const [internalTab, setInternalTab] = useState(tabs?.[0]?.id ?? 'all')
|
||||
const activeTab = controlledTab ?? internalTab
|
||||
|
||||
@@ -207,21 +205,15 @@ export function ResourcePage<T extends object>({
|
||||
resetPagination()
|
||||
}, [onClearFilters, resetPagination])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isError || (data.length === 0 && emptyState)) return
|
||||
const el = frameRef.current
|
||||
if (!el) return
|
||||
debugAgentLog(
|
||||
'resource-page.tsx:mount',
|
||||
'resource page width',
|
||||
{
|
||||
title,
|
||||
clientWidth: el.clientWidth,
|
||||
maxWidth: getComputedStyle(el).maxWidth,
|
||||
},
|
||||
'A',
|
||||
)
|
||||
}, [title, isLoading, isError, data.length, emptyState])
|
||||
const countedTabs = useMemo(
|
||||
() =>
|
||||
(tabs ?? []).map((tab) => ({
|
||||
id: tab.id,
|
||||
label: tab.label,
|
||||
count: tabCounts[tab.id] ?? tab.count ?? 0,
|
||||
})),
|
||||
[tabs, tabCounts],
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return <ResourcePageSkeleton />
|
||||
@@ -256,7 +248,7 @@ export function ResourcePage<T extends object>({
|
||||
const emptyMessage = 'Нет записей по выбранным фильтрам.'
|
||||
|
||||
return (
|
||||
<div ref={frameRef} className="w-full">
|
||||
<div className="w-full">
|
||||
{selectionToolbar && selectedCount > 0
|
||||
? selectionToolbar({
|
||||
selectedIds,
|
||||
@@ -306,26 +298,14 @@ export function ResourcePage<T extends object>({
|
||||
) : null}
|
||||
|
||||
<FramePanel className="p-0 shadow-none!">
|
||||
{tabs && tabs.length > 0 ? (
|
||||
{countedTabs.length > 0 ? (
|
||||
<>
|
||||
{/* Line tabs: c-tabs-2 underline; after:bottom-0 — FramePanel overflow-hidden клипает bottom-[-5px] */}
|
||||
<div className="px-(--frame-panel-header-px) pt-(--frame-panel-header-py)">
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
||||
<TabsList variant="line" className="gap-5">
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
||||
>
|
||||
<span>{tab.label}</span>
|
||||
<span className="bg-muted text-muted-foreground inline-flex min-w-5 items-center justify-center rounded-md px-1.5 py-0.5 text-xs tabular-nums">
|
||||
{tabCounts[tab.id] ?? tab.count ?? 0}
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<CountedLineTabs
|
||||
tabs={countedTabs}
|
||||
value={activeTab}
|
||||
onValueChange={handleTabChange}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
</>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useEffect, useRef, type ReactNode } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { Link, Outlet, useRouterState } from '@tanstack/react-router'
|
||||
import { SettingsIcon } from 'lucide-react'
|
||||
|
||||
import { useIsMobile } from '@cfdm/ui/hooks/use-mobile'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
import { PageShell } from '@/components/page-shell'
|
||||
import { debugAgentLog } from '@/lib/debug-agent-log'
|
||||
|
||||
export interface SettingsTabConfig {
|
||||
id: string
|
||||
@@ -35,28 +34,11 @@ export function SettingsShell({
|
||||
tabs = DEFAULT_TABS,
|
||||
}: SettingsShellProps) {
|
||||
const isMobile = useIsMobile()
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||
|
||||
useEffect(() => {
|
||||
const el = rootRef.current
|
||||
if (!el) return
|
||||
debugAgentLog(
|
||||
'settings-shell.tsx:mount',
|
||||
'settings shell width',
|
||||
{
|
||||
pathname,
|
||||
clientWidth: el.clientWidth,
|
||||
maxWidth: getComputedStyle(el).maxWidth,
|
||||
className: el.className,
|
||||
},
|
||||
'A',
|
||||
)
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<PageShell>
|
||||
<div ref={rootRef} className="mx-auto flex w-full max-w-4xl flex-col gap-6">
|
||||
<div className="mx-auto flex w-full max-w-4xl flex-col gap-6">
|
||||
<header className="px-1">
|
||||
<div className="flex flex-col gap-px">
|
||||
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
|
||||
@@ -78,7 +60,7 @@ export function SettingsShell({
|
||||
className={cn(
|
||||
'flex gap-1',
|
||||
isMobile
|
||||
? 'scrollbar-none -mx-1 overflow-x-auto pb-1'
|
||||
? 'scrollbar-none -mx-1 overflow-x-auto overflow-y-hidden pb-1'
|
||||
: 'w-44 shrink-0 flex-col',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
FramePanel,
|
||||
FrameTitle,
|
||||
} from '@/components/reui/frame'
|
||||
import { CountedLineTabs } from '@/components/counted-line-tabs'
|
||||
import { EmptyState } from '@/components/empty-state'
|
||||
import { applyFiltersToData } from '@/components/reui-kit/filter-utils'
|
||||
import { createServicesGroupedColumns } from '@/components/services/services-grouped-columns'
|
||||
@@ -53,7 +54,6 @@ import {
|
||||
} from '@cfdm/ui/components/dropdown-menu'
|
||||
import { Separator } from '@cfdm/ui/components/separator'
|
||||
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
|
||||
|
||||
const HEALTH_TABS = [
|
||||
{ id: 'health-ok', label: 'OK' },
|
||||
@@ -359,22 +359,15 @@ export function ServicesGroupedCatalog({
|
||||
|
||||
<FramePanel className="p-0 shadow-none!">
|
||||
<div className="px-(--frame-panel-header-px) pt-(--frame-panel-header-py)">
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<TabsList variant="line" className="gap-5 overflow-x-auto">
|
||||
{ALL_TABS.map((t) => (
|
||||
<TabsTrigger
|
||||
key={t.id}
|
||||
value={t.id}
|
||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
||||
>
|
||||
<span>{t.label}</span>
|
||||
<span className="bg-muted text-muted-foreground inline-flex min-w-5 items-center justify-center rounded-md px-1.5 py-0.5 text-xs tabular-nums">
|
||||
{tabCounts[t.id] ?? 0}
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<CountedLineTabs
|
||||
tabs={ALL_TABS.map((t) => ({
|
||||
id: t.id,
|
||||
label: t.label,
|
||||
count: tabCounts[t.id] ?? 0,
|
||||
}))}
|
||||
value={tab}
|
||||
onValueChange={setTab}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -67,8 +67,8 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
||||
"group-data-[variant=line]/tabs-list:flex-none group-data-[variant=line]/tabs-list:rounded-none group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:shadow-none group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=line]/tabs-list:data-active:shadow-none dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
||||
"group-data-[variant=default]/tabs-list:data-active:bg-background group-data-[variant=default]/tabs-list:data-active:text-foreground dark:group-data-[variant=default]/tabs-list:data-active:border-input dark:group-data-[variant=default]/tabs-list:data-active:bg-input/30 dark:group-data-[variant=default]/tabs-list:data-active:text-foreground",
|
||||
"data-active:text-foreground",
|
||||
// Line underline (c-tabs-2): size/position need data-horizontal on root; active → opacity-100
|
||||
"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",
|
||||
// Line underline (c-tabs-2): bottom-0 so FramePanel overflow-hidden does not clip
|
||||
"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-0 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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user