Commit db48108d by CaIon

feat(logs): enhance usage logs table with log type indicators and improve UI elements

parent 22ef5b2f
...@@ -19,7 +19,7 @@ import { ...@@ -19,7 +19,7 @@ import {
type DataTableColumnHeaderProps<TData, TValue> = type DataTableColumnHeaderProps<TData, TValue> =
React.HTMLAttributes<HTMLDivElement> & { React.HTMLAttributes<HTMLDivElement> & {
column: Column<TData, TValue> column: Column<TData, TValue>
title: string title: React.ReactNode
} }
export function DataTableColumnHeader<TData, TValue>({ export function DataTableColumnHeader<TData, TValue>({
......
...@@ -69,7 +69,7 @@ export interface StatusBadgeProps extends Omit< ...@@ -69,7 +69,7 @@ export interface StatusBadgeProps extends Omit<
children?: React.ReactNode children?: React.ReactNode
icon?: LucideIcon icon?: LucideIcon
pulse?: boolean pulse?: boolean
/** @deprecated Dot is always shown in flat design */ /** When false, hides the leading dot */
showDot?: boolean showDot?: boolean
variant?: StatusVariant | null variant?: StatusVariant | null
size?: 'sm' | 'md' | 'lg' | null size?: 'sm' | 'md' | 'lg' | null
...@@ -87,7 +87,7 @@ export function StatusBadge({ ...@@ -87,7 +87,7 @@ export function StatusBadge({
variant, variant,
size = 'sm', size = 'sm',
pulse = false, pulse = false,
showDot: _showDot, showDot = true,
rounded: _rounded, rounded: _rounded,
copyable = true, copyable = true,
copyText, copyText,
...@@ -110,7 +110,7 @@ export function StatusBadge({ ...@@ -110,7 +110,7 @@ export function StatusBadge({
onClick?.(e) onClick?.(e)
} }
const content = children ?? (label ? <span>{label}</span> : null) const content = children ?? (label ? <span className='truncate'>{label}</span> : null)
return ( return (
<span <span
...@@ -127,14 +127,16 @@ export function StatusBadge({ ...@@ -127,14 +127,16 @@ export function StatusBadge({
title={copyable ? `Click to copy: ${copyText || label || ''}` : undefined} title={copyable ? `Click to copy: ${copyText || label || ''}` : undefined}
{...props} {...props}
> >
<span {showDot && (
className={cn( <span
'inline-block size-1.5 shrink-0 rounded-full', className={cn(
dotColorMap[computedVariant] 'inline-block size-1.5 shrink-0 rounded-full',
)} dotColorMap[computedVariant]
aria-hidden='true' )}
/> aria-hidden='true'
{Icon && <Icon className='h-3 w-3' />} />
)}
{Icon && <Icon className='size-3 shrink-0' />}
{content} {content}
</span> </span>
) )
......
import { useState, useEffect, useCallback, type ReactNode } from 'react' import { useState, useEffect, useCallback, type ReactNode } from 'react'
import { useNavigate, getRouteApi } from '@tanstack/react-router' import { useNavigate, getRouteApi } from '@tanstack/react-router'
import { ChevronDown, Eye, EyeOff, RotateCcw, Search } from 'lucide-react' import { useQueryClient, useIsFetching } from '@tanstack/react-query'
import { ChevronDown, Eye, EyeOff, Loader2, RotateCcw, Search } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useIsAdmin } from '@/hooks/use-admin' import { useIsAdmin } from '@/hooks/use-admin'
...@@ -33,9 +34,11 @@ export function CommonLogsFilterBar({ ...@@ -33,9 +34,11 @@ export function CommonLogsFilterBar({
}: CommonLogsFilterBarProps) { }: CommonLogsFilterBarProps) {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const queryClient = useQueryClient()
const searchParams = route.useSearch() const searchParams = route.useSearch()
const isAdmin = useIsAdmin() const isAdmin = useIsAdmin()
const { sensitiveVisible, setSensitiveVisible } = useUsageLogsContext() const { sensitiveVisible, setSensitiveVisible } = useUsageLogsContext()
const fetchingLogs = useIsFetching({ queryKey: ['logs'] })
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
const [filters, setFilters] = useState<CommonLogFilters>(() => { const [filters, setFilters] = useState<CommonLogFilters>(() => {
...@@ -88,14 +91,15 @@ export function CommonLogsFilterBar({ ...@@ -88,14 +91,15 @@ export function CommonLogsFilterBar({
navigate({ navigate({
to: '/usage-logs/$section', to: '/usage-logs/$section',
params: { section: 'common' }, params: { section: 'common' },
search: (prev: Record<string, unknown>) => ({ search: {
...prev,
...filterParams, ...filterParams,
...(logType ? { type: [logType] } : { type: undefined }), ...(logType ? { type: [logType] } : {}),
page: 1, page: 1,
}), },
}) })
}, [filters, logType, navigate]) queryClient.invalidateQueries({ queryKey: ['logs'] })
queryClient.invalidateQueries({ queryKey: ['usage-logs-stats'] })
}, [filters, logType, navigate, queryClient])
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
const { start, end } = getDefaultTimeRange() const { start, end } = getDefaultTimeRange()
...@@ -112,7 +116,9 @@ export function CommonLogsFilterBar({ ...@@ -112,7 +116,9 @@ export function CommonLogsFilterBar({
endTime: end.getTime(), endTime: end.getTime(),
}, },
}) })
}, [navigate]) queryClient.invalidateQueries({ queryKey: ['logs'] })
queryClient.invalidateQueries({ queryKey: ['usage-logs-stats'] })
}, [navigate, queryClient])
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
...@@ -267,8 +273,12 @@ export function CommonLogsFilterBar({ ...@@ -267,8 +273,12 @@ export function CommonLogsFilterBar({
<RotateCcw className='size-3.5' /> <RotateCcw className='size-3.5' />
{t('Reset')} {t('Reset')}
</Button> </Button>
<Button size='sm' className='h-8' onClick={handleApply}> <Button size='sm' className='h-8' onClick={handleApply} disabled={fetchingLogs > 0}>
<Search className='size-3.5' /> {fetchingLogs > 0 ? (
<Loader2 className='size-3.5 animate-spin' />
) : (
<Search className='size-3.5' />
)}
{t('Search')} {t('Search')}
</Button> </Button>
{viewOptions} {viewOptions}
......
...@@ -42,19 +42,19 @@ export function CommonLogsStats() { ...@@ -42,19 +42,19 @@ export function CommonLogsStats() {
if (isLoading) { if (isLoading) {
return ( return (
<div className='flex items-center gap-1.5'> <div className='flex items-center gap-2'>
<Skeleton className='h-6 w-[126px] rounded-md' /> <Skeleton className='h-7 w-[150px] rounded-md' />
<Skeleton className='h-6 w-[76px] rounded-md' /> <Skeleton className='h-7 w-[100px] rounded-md' />
<Skeleton className='h-6 w-[92px] rounded-md' /> <Skeleton className='h-7 w-[120px] rounded-md' />
</div> </div>
) )
} }
const tagClass = const tagClass =
'inline-flex h-6 items-center rounded-md border px-2.5 text-xs font-medium shadow-xs' 'inline-flex h-7 items-center rounded-md border px-3 py-1 text-xs font-medium shadow-xs'
return ( return (
<div className='flex flex-wrap items-center gap-1.5'> <div className='flex flex-wrap items-center gap-2'>
<span <span
className={cn( className={cn(
tagClass, tagClass,
......
...@@ -34,7 +34,7 @@ import { ...@@ -34,7 +34,7 @@ import {
MobileCardList, MobileCardList,
} from '@/components/data-table' } from '@/components/data-table'
import { PageFooterPortal } from '@/components/layout' import { PageFooterPortal } from '@/components/layout'
import { DEFAULT_LOGS_DATA } from '../constants' import { DEFAULT_LOGS_DATA, LOG_TYPE_ENUM } from '../constants'
import { useColumnsByCategory } from '../lib/columns' import { useColumnsByCategory } from '../lib/columns'
import { fetchLogsByCategory } from '../lib/utils' import { fetchLogsByCategory } from '../lib/utils'
import type { LogCategory } from '../types' import type { LogCategory } from '../types'
...@@ -43,6 +43,20 @@ import { CommonLogsStats } from './common-logs-stats' ...@@ -43,6 +43,20 @@ import { CommonLogsStats } from './common-logs-stats'
const route = getRouteApi('/_authenticated/usage-logs/$section') const route = getRouteApi('/_authenticated/usage-logs/$section')
const logTypeBorderColor: Record<number, string> = {
[LOG_TYPE_ENUM.TOPUP]: 'border-l-cyan-400 dark:border-l-cyan-500',
[LOG_TYPE_ENUM.CONSUME]: 'border-l-emerald-400 dark:border-l-emerald-500',
[LOG_TYPE_ENUM.MANAGE]: 'border-l-orange-400 dark:border-l-orange-500',
[LOG_TYPE_ENUM.SYSTEM]: 'border-l-purple-400 dark:border-l-purple-500',
[LOG_TYPE_ENUM.ERROR]: 'border-l-rose-400 dark:border-l-rose-500',
[LOG_TYPE_ENUM.REFUND]: 'border-l-blue-400 dark:border-l-blue-500',
}
const logTypeRowTint: Record<number, string> = {
[LOG_TYPE_ENUM.ERROR]: 'bg-rose-50/40 dark:bg-rose-950/20',
[LOG_TYPE_ENUM.REFUND]: 'bg-blue-50/30 dark:bg-blue-950/15',
}
interface UsageLogsTableProps { interface UsageLogsTableProps {
logCategory: LogCategory logCategory: LogCategory
} }
...@@ -150,6 +164,42 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) { ...@@ -150,6 +164,42 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
ensurePageInRange(pageCount) ensurePageInRange(pageCount)
}, [pageCount, ensurePageInRange]) }, [pageCount, ensurePageInRange])
const isCommon = logCategory === 'common'
const renderRows = () => {
const rows = table.getRowModel().rows
if (rows.length === 0) return null
return rows.map((row) => {
const logType = (row.original as Record<string, unknown>).type as
| number
| undefined
const borderClass =
isCommon && logType != null
? logTypeBorderColor[logType] ?? 'border-l-transparent'
: ''
const tintClass =
isCommon && logType != null ? (logTypeRowTint[logType] ?? '') : ''
return (
<TableRow
key={row.id}
className={cn(
'!border-l-[3px] transition-colors',
borderClass,
tintClass
)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className='py-2'>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
})
}
return ( return (
<> <>
<div className='space-y-4'> <div className='space-y-4'>
...@@ -184,9 +234,9 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) { ...@@ -184,9 +234,9 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
)} )}
> >
<Table> <Table>
<TableHeader> <TableHeader className='bg-muted/30 sticky top-0 z-10'>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id} className='border-l-[3px] border-l-transparent'>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<TableHead key={header.id} colSpan={header.colSpan}> <TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder {header.isPlaceholder
...@@ -212,18 +262,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) { ...@@ -212,18 +262,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
)} )}
/> />
) : ( ) : (
table.getRowModel().rows.map((row) => ( renderRows()
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className='py-2'>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
)} )}
</TableBody> </TableBody>
</Table> </Table>
......
...@@ -162,7 +162,6 @@ export function formatTokens(tokens: number): string { ...@@ -162,7 +162,6 @@ export function formatTokens(tokens: number): string {
* Format use time in seconds with appropriate unit * Format use time in seconds with appropriate unit
*/ */
export function formatUseTime(seconds: number): string { export function formatUseTime(seconds: number): string {
if (seconds < 1) return `${(seconds * 1000).toFixed(0)}ms`
if (seconds < 60) return `${seconds.toFixed(1)}s` if (seconds < 60) return `${seconds.toFixed(1)}s`
const minutes = Math.floor(seconds / 60) const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60 const remainingSeconds = seconds % 60
......
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
:root { :root {
--background: oklch(1 0 0); --background: oklch(0.994 0.002 247.858);
--foreground: oklch(0.129 0.042 264.695); --foreground: oklch(0.18 0.035 264.695);
--card: oklch(1 0 0); --card: oklch(0.997 0.002 247.858);
--card-foreground: oklch(0.129 0.042 264.695); --card-foreground: oklch(0.18 0.035 264.695);
--popover: oklch(1 0 0); --popover: oklch(0.997 0.002 247.858);
--popover-foreground: oklch(0.129 0.042 264.695); --popover-foreground: oklch(0.18 0.035 264.695);
--primary: oklch(0.208 0.042 265.755); --primary: oklch(0.255 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858); --primary-foreground: oklch(0.985 0.004 247.858);
--secondary: oklch(0.968 0.007 247.896); --secondary: oklch(0.974 0.004 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755); --secondary-foreground: oklch(0.255 0.042 265.755);
--muted: oklch(0.968 0.007 247.896); --muted: oklch(0.972 0.004 247.896);
--muted-foreground: oklch(0.554 0.046 257.417); --muted-foreground: oklch(0.49 0.04 257.417);
--accent: oklch(0.968 0.007 247.896); --accent: oklch(0.972 0.004 247.896);
--accent-foreground: oklch(0.208 0.042 265.755); --accent-foreground: oklch(0.255 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508); --border: oklch(0.925 0.01 255.508);
--input: oklch(0.929 0.013 255.508); --input: oklch(0.925 0.01 255.508);
--ring: oklch(0.704 0.04 256.788); --ring: oklch(0.64 0.055 256.788);
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704); --chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392); --chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429); --chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08); --chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem; --radius: 0.625rem;
--sidebar: var(--background); --sidebar: oklch(0.991 0.002 247.858);
--sidebar-foreground: var(--foreground); --sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary); --sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground); --sidebar-primary-foreground: var(--primary-foreground);
...@@ -33,44 +33,44 @@ ...@@ -33,44 +33,44 @@
--sidebar-accent-foreground: var(--accent-foreground); --sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border); --sidebar-border: var(--border);
--sidebar-ring: var(--ring); --sidebar-ring: var(--ring);
--skeleton-base: oklch(0.943 0.003 264); --skeleton-base: oklch(0.948 0.004 264);
--skeleton-highlight: oklch(0.985 0.001 264); --skeleton-highlight: oklch(0.988 0.002 264);
} }
.dark { .dark {
--background: oklch(0.11 0 0); --background: oklch(0.165 0.012 258);
--foreground: oklch(0.984 0.003 247.858); --foreground: oklch(0.92 0.008 247.858);
--card: oklch(0.13 0 0); --card: oklch(0.205 0.012 258);
--card-foreground: oklch(0.984 0.003 247.858); --card-foreground: oklch(0.92 0.008 247.858);
--popover: oklch(0.15 0 0); --popover: oklch(0.225 0.014 258);
--popover-foreground: oklch(0.984 0.003 247.858); --popover-foreground: oklch(0.92 0.008 247.858);
--primary: oklch(0.929 0.013 255.508); --primary: oklch(0.87 0.018 255.508);
--primary-foreground: oklch(0.208 0.042 265.755); --primary-foreground: oklch(0.235 0.042 265.755);
--secondary: oklch(0.2 0 0); --secondary: oklch(0.255 0.012 258);
--secondary-foreground: oklch(0.984 0.003 247.858); --secondary-foreground: oklch(0.92 0.008 247.858);
--muted: oklch(0.2 0 0); --muted: oklch(0.245 0.012 258);
--muted-foreground: oklch(0.704 0 0); --muted-foreground: oklch(0.68 0.014 257.417);
--accent: oklch(0.2 0 0); --accent: oklch(0.265 0.014 258);
--accent-foreground: oklch(0.984 0.003 247.858); --accent-foreground: oklch(0.92 0.008 247.858);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 12%); --border: oklch(0.31 0.014 258);
--input: oklch(1 0 0 / 16%); --input: oklch(0.34 0.014 258);
--ring: oklch(0.65 0 0); --ring: oklch(0.58 0.025 256.788);
--chart-1: oklch(0.488 0.243 264.376); --chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48); --chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08); --chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9); --chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439); --chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0); --sidebar: oklch(0.185 0.012 258);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.92 0.008 247.858);
--sidebar-primary: oklch(0.75 0.14 233); --sidebar-primary: oklch(0.75 0.14 233);
--sidebar-primary-foreground: oklch(0.29 0.06 243); --sidebar-primary-foreground: oklch(0.29 0.06 243);
--sidebar-accent: oklch(0.269 0 0); --sidebar-accent: oklch(0.255 0.012 258);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.92 0.008 247.858);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(0.3 0.014 258);
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.52 0.02 256.788);
--skeleton-base: oklch(0.18 0 0); --skeleton-base: oklch(0.245 0.01 258);
--skeleton-highlight: oklch(0.26 0 0); --skeleton-highlight: oklch(0.32 0.014 258);
} }
@theme inline { @theme inline {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment