feat(studio): replace auth overview charts (#41464)

* feat: replace usage metric cards with new charts

* feat: use new chart components for auth api and error codes

* style: some tweaks to the two tables

* fix: padding around empty states

* feat: smol adjustment

* fix: add aria label to go to logs tooltip

* fix: diff value

* fix: status code method font colour

* fix: actions onClick
This commit is contained in:
kemal.earth authored and GitHub committed 2026-01-22 12:56:32 +00:00
1 parent 434d8735f3
commit ee25ba20dc
3 files changed
+215 -197

No files matched your search

@@ -1,25 +1,13 @@
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { ChevronRight, ExternalLink, HelpCircle, Telescope } from 'lucide-react'
import { ChevronRight, ExternalLink, Telescope, BarChart2, Bot } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { getStatusLevel } from 'components/interfaces/UnifiedLogs/UnifiedLogs.utils'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DataTableColumnStatusCode } from 'components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode'
import {
Card,
CardContent,
CardHeader,
CardTitle,
cn,
Skeleton,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { cn, Tooltip, TooltipContent, TooltipTrigger, Button } from 'ui'
import {
PageSection,
PageSectionContent,
@@ -27,6 +15,17 @@ import {
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import {
Chart,
ChartCard,
ChartHeader,
ChartActions,
ChartMetric,
ChartTitle,
ChartContent,
ChartEmptyState,
ChartLoadingState,
} from 'ui-patterns/Chart'
import {
AuthErrorCodeRow,
fetchTopAuthErrorCodes,
@@ -39,9 +38,13 @@ import {
calculatePercentageChange,
getApiSuccessRates,
getAuthSuccessRates,
getChangeColor,
getMetricValues,
} from './OverviewUsage.constants'
import { getStatusColor } from 'components/ui/DataTable/DataTable.utils'
import { AiIconAnimation } from 'ui'
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
const StatCard = ({
title,
@@ -63,84 +66,55 @@ const StatCard = ({
tooltip?: string
}) => {
const router = useRouter()
const isZeroChange = previous === 0
const changeColor = isZeroChange
? 'text-foreground-lighter'
: invert
? previous >= 0
? 'text-destructive'
: 'text-brand'
: getChangeColor(previous)
const formattedCurrent =
suffix === 'ms'
? current.toFixed(2)
: suffix === '%'
? current.toFixed(1)
: Math.round(current).toLocaleString()
const signChar = previous > 0 ? '+' : previous < 0 ? '-' : ''
// const signChar = previous > 0 ? '+' : previous < 0 ? '-' : ''
const actions = [
{
label: 'Go to Auth Report',
icon: <ExternalLink size={12} />,
onClick: href ? () => router.push(href) : undefined,
},
]
return (
<Card className={cn(href, 'mb-0 flex flex-col')}>
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-0 border-b-0 relative">
<CardTitle className="text-foreground-light flex items-center gap-2">
{title}
{tooltip && (
<Tooltip>
<TooltipTrigger>
<HelpCircle className="text-foreground-light" size={14} strokeWidth={1.5} />
</TooltipTrigger>
<TooltipContent className="w-[300px]">
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
)}
</CardTitle>
<ButtonTooltip
type="text"
size="tiny"
icon={<ExternalLink />}
className="w-6 h-6 absolute right-4 top-3"
onClick={() => router.push(href || '')}
tooltip={{
content: {
side: 'top',
text: 'Go to Auth Report',
},
}}
/>
</CardHeader>
<CardContent
className={cn(
'pb-4 px-6 pt-0 flex-1 h-full overflow-hidden',
loading && 'pt-2 opacity-50 items-center justify-center'
)}
>
{loading ? (
<div className="flex flex-col gap-2">
<Skeleton className="h-6 w-20" />
<Skeleton className="h-3 w-8" />
</div>
) : (
<div className="flex flex-col gap-0.5">
<p className="text-xl">{`${formattedCurrent}${suffix}`}</p>
<span className={cn('flex items-center gap-1 text-sm', changeColor)}>
<span>{`${signChar}${Math.abs(previous).toFixed(1)}%`}</span>
</span>
</div>
)}
</CardContent>
</Card>
<Chart isLoading={loading}>
<ChartCard>
<ChartHeader align="start">
<ChartMetric
className="pb-4"
label={title}
tooltip={tooltip}
diffValue={`${previous.toFixed(1)}%`}
value={`${formattedCurrent}${suffix}`}
/>
<ChartActions actions={actions} />
</ChartHeader>
</ChartCard>
</Chart>
)
}
const LogsLink = ({ href }: { href: string }) => (
<Tooltip>
<TooltipTrigger asChild>
<Link className="block text-foreground-lighter hover:text-foreground p-1.5" href={href}>
<ChevronRight className="size-4" />
</Link>
<Button
type="text"
size="tiny"
className="p-1.5 text-foreground-lighter hover:text-foreground"
asChild
>
<Link href={href} aria-label="Go to Logs">
<ChevronRight size={12} />
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Go to logs</TooltipContent>
<TooltipContent>Go to Logs</TooltipContent>
</Tooltip>
)
@@ -171,6 +145,8 @@ export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsPr
const { ref } = useParams()
const endDate = dayjs().toISOString()
const startDate = dayjs().subtract(24, 'hour').toISOString()
const aiSnap = useAiAssistantStateSnapshot()
const { openSidebar } = useSidebarManagerSnapshot()
const { current: activeUsersCurrent, previous: activeUsersPrevious } = getMetricValues(
metrics,
@@ -218,6 +194,20 @@ export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsPr
? (codeErrData?.result as unknown[]).filter(isAuthErrorCodeRow)
: []
const errorCodesActions = [
{
label: 'Ask Assistant about Error Codes',
icon: <AiIconAnimation size={12} />,
onClick: () => {
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
aiSnap.newChat({
name: 'Auth Help',
initialInput: `Can you explain to me what the authentication error codes mean?`,
})
},
},
]
return (
<>
<PageSection>
@@ -236,11 +226,11 @@ export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsPr
<PageSectionTitle>Usage</PageSectionTitle>
<Link
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}&isHelper=true&helperText=Last+24+hours`}
className="text-sm text-link inline-flex items-center gap-x-1.5"
className="text-foreground underline underline-offset-2 decoration-foreground-muted hover:decoration-foreground transition-all text-sm inline-flex items-center gap-x-1.5"
>
<Telescope size={14} />
<Telescope size={14} className="text-foreground-lighter" />
<span>Go to observability</span>
<ChevronRight size={14} />
<ChevronRight size={14} className="text-foreground-lighter" />
</Link>
</div>
</PageSectionSummary>
@@ -292,112 +282,143 @@ export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsPr
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<CardHeader className={cn('border-b-0', responseErrors.length > 0 ? 'pb-4' : 'pb-0')}>
<CardTitle className="text-foreground-light">Auth API Errors</CardTitle>
</CardHeader>
<CardContent className="p-0">
<OverviewTable<ResponseErrorRow>
isLoading={isLoadingResp}
data={responseErrors}
columns={[
{
key: 'request',
header: 'Request',
className: 'w-[60px]',
render: (row) => (
<span className="font-mono text-xs truncate select-text cursor-text py-1 px-1.5 text-center rounded-md bg-alternative-200">
{row.method}
</span>
),
},
{
key: 'status_code',
header: 'Status',
className: 'w-[60px]',
render: (row) => (
<DataTableColumnStatusCode
value={row.status_code}
level={getStatusLevel(row.status_code)}
className="text-sm"
/>
),
},
{
key: 'path',
header: 'Path',
className: 'flex-shrink-0 w-52',
render: (row) => (
<div className="line-clamp-1 font-mono text-foreground-light text-xs">
{row.path}
</div>
),
},
{
key: 'count',
header: 'Count',
className: 'text-right flex-shrink-0 ml-auto justify-end',
render: (row) => (
<div className="text-right text-xs tabular-nums">{row.count}</div>
),
},
{
key: 'actions',
header: '',
className: 'w-6',
render: (row) => (
<div className="flex justify-end">
<LogsLink href={`/project/${ref}/logs/edge-logs?s=${row.path}`} />
</div>
),
},
]}
/>
</CardContent>
</Card>
<div className="grid grid-cols-1 gap-4">
<Chart isLoading={isLoadingResp}>
<ChartCard>
<ChartHeader>
<ChartTitle>Auth API Errors</ChartTitle>
</ChartHeader>
<ChartContent
className="p-0"
isEmpty={responseErrors.length === 0}
emptyState={
<div className="p-6">
<ChartEmptyState
icon={<BarChart2 size={16} />}
title="No data to show"
description="It may take up to 24 hours for data to refresh"
/>
</div>
}
loadingState={
<div className="p-6">
<ChartLoadingState />
</div>
}
>
<OverviewTable<ResponseErrorRow>
isLoading={isLoadingResp}
data={responseErrors}
columns={[
{
key: 'request',
header: 'Request',
className: 'w-auto !pr-0',
render: (row) => {
const level = getStatusLevel(row.status_code)
const colors = getStatusColor(level)
return (
<div className="flex items-center gap-2">
<span className="flex-shrink-0 flex items-center text-xs font-mono">
<span className="select-text py-0.5 px-2 text-center rounded-l rounded-r-none bg-surface-75 text-foreground-light border border-r-0">
{row.method}
</span>
<span
className={cn(
'py-0.5 px-2 border rounded-l-0 rounded-r tabular-nums',
colors.text,
colors.bg,
colors.border
)}
>
{row.status_code}
</span>
</span>
</div>
)
},
},
{
key: 'path',
header: 'Path',
className: 'w-full',
render: (row) => (
<span className="line-clamp-1 font-mono text-foreground-light text-xs">
{row.path}
</span>
),
},
{
key: 'count',
header: 'Count',
className: 'text-right flex-shrink-0 ml-auto justify-end',
render: (row) => (
<div className="flex justify-end items-center gap-2">
<div className="text-right text-xs tabular-nums">{row.count}</div>
<LogsLink href={`/project/${ref}/logs/edge-logs?s=${row.path}`} />
</div>
),
},
]}
/>
</ChartContent>
</ChartCard>
</Chart>
<Card>
<CardHeader className={cn('border-b-0', errorCodes.length > 0 ? 'pb-4' : 'pb-0')}>
<CardTitle className="text-foreground-light">Auth Server Errors</CardTitle>
</CardHeader>
<CardContent className="p-0">
<OverviewTable<AuthErrorCodeRow>
isLoading={isLoadingCodes}
data={errorCodes}
columns={[
{
key: 'error_code',
header: 'Error code',
className: 'w-full',
render: (row) => (
<div className="line-clamp-1 font-mono text-foreground uppercase text-xs">
{row.error_code}
</div>
),
},
{
key: 'count',
header: 'Count',
className: 'text-right',
render: (row) => (
<div className="text-right text-xs tabular-nums">{row.count}</div>
),
},
{
key: 'actions',
header: '',
className: 'text-right',
render: (row) => (
<div>
<LogsLink href={`/project/${ref}/logs/auth-logs?s=${row.error_code}`} />
</div>
),
},
]}
/>
</CardContent>
</Card>
<Chart isLoading={isLoadingCodes}>
<ChartCard>
<ChartHeader>
<ChartTitle>Auth Server Errors</ChartTitle>
<ChartActions actions={errorCodesActions} />
</ChartHeader>
<ChartContent
className="p-0"
isEmpty={errorCodes.length === 0}
emptyState={
<div className="p-6">
<ChartEmptyState
icon={<BarChart2 size={16} />}
title="No data to show"
description="It may take up to 24 hours for data to refresh"
/>
</div>
}
loadingState={
<div className="p-6">
<ChartLoadingState />
</div>
}
>
<OverviewTable<AuthErrorCodeRow>
isLoading={isLoadingCodes}
data={errorCodes}
columns={[
{
key: 'error_code',
header: 'Error code',
className: 'w-full',
render: (row) => (
<span className="line-clamp-1 font-mono uppercase text-xs inline-flex text-foreground-light">
{row.error_code}
</span>
),
},
{
key: 'count',
header: 'Count',
className: 'text-right',
render: (row) => (
<div className="flex justify-end items-center gap-2">
<div className="text-right text-xs tabular-nums">{row.count}</div>
<LogsLink href={`/project/${ref}/logs/auth-logs?s=${row.error_code}`} />
</div>
),
},
]}
/>
</ChartContent>
</ChartCard>
</Chart>
</div>
</PageSectionContent>
</PageSection>
@@ -21,15 +21,12 @@ export function OverviewTable<T>({ columns, data, isLoading, emptyMessage }: Ove
const hasData = !isLoading && data.length > 0
return (
<Table>
<Table className="border-t mt-4">
{hasData && (
<TableHeader>
<TableRow>
{columns.map((col) => (
<TableHead
key={String(col.key)}
className={cn(col.className, 'bg-surface-200 border-t h-8 px-6')}
>
<TableHead key={String(col.key)} className={cn(col.className)}>
{col.header}
</TableHead>
))}
@@ -61,14 +58,13 @@ export function OverviewTable<T>({ columns, data, isLoading, emptyMessage }: Ove
(data as unknown as T[]).map((row, idx) => (
<TableRow key={idx}>
{columns.map((col) => (
<TableCell key={String(col.key)} className={cn('py-1.5 pl-6 pr-4', col.className)}>
<TableCell key={String(col.key)} className={cn(col.className)}>
{col.render ? col.render(row) : (row as any)[col.key as string]}
</TableCell>
))}
</TableRow>
))
)}
{data.length > 0 && <span className="h-2 flex w-full" />}
</TableBody>
</Table>
)
+5 -4
View File
@@ -62,6 +62,8 @@ interface ChartProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string
}
const chartTableClasses = `[&_tr]:border-b [&_tr]:border-border [&_thead_tr]:!bg-transparent [&_thead_th]:!py-2 [&_thead_th]:!px-6 [&_thead_th]:h-auto [&_tbody_td]:py-2.5 [&_tbody_td]:px-6 [&_tbody_td]:text-xs [&_table]:mb-1 [&_table]:border-b [&_table]:border-border`
const Chart = React.forwardRef<HTMLDivElement, ChartProps>(
({ children, isLoading = false, isDisabled = false, className, ...props }, ref) => {
return (
@@ -138,7 +140,7 @@ const ChartTitle = React.forwardRef<HTMLDivElement, ChartTitleProps>(
className="text-foreground-lighter hover:text-foreground-light transition-colors cursor-help"
/>
</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
<TooltipContent className="max-w-72">{tooltip}</TooltipContent>
</Tooltip>
)}
</h3>
@@ -280,7 +282,7 @@ const ChartMetric = React.forwardRef<HTMLDivElement, ChartMetricProps>(
className="text-foreground-lighter hover:text-foreground-light transition-colors cursor-help"
/>
</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
<TooltipContent className="max-w-72">{tooltip}</TooltipContent>
</Tooltip>
)}
</h3>
@@ -336,7 +338,7 @@ const ChartContent = React.forwardRef<HTMLDivElement, ChartContentProps>(
}
return (
<div ref={ref} className={cn('px-6 pt-4 pb-6', className)} {...props}>
<div ref={ref} className={cn('px-6 pt-4 pb-6', chartTableClasses, className)} {...props}>
{content}
</div>
)
@@ -478,7 +480,6 @@ const ChartDisabledState = ({ icon, label, description, actions }: ChartDisabled
ChartDisabledState.displayName = 'ChartDisabledState'
/* Chart Footer */
const chartTableClasses = `[&_tr]:border-b [&_tr]:border-border [&_thead_tr]:!bg-transparent [&_thead_th]:!py-2 [&_thead_th]:h-auto [&_tbody_td]:py-2.5 [&_tbody_td]:text-xs [&_table]:mb-1 [&_table]:border-b [&_table]:border-border`
interface ChartFooterProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode