Files
supabase/apps/studio/components/interfaces/Auth/Overview/OverviewMetrics.tsx
Ivan Vasilov 56de26fe22 chore: Migrate the monorepo to use Tailwind v4 (#45318)
This PR migrates the whole monorepo to use Tailwind v4:
- Removed `@tailwindcss/container-queries` plugin since it's included by
default in v4,
- Bump all instances of Tailwind to v4. Made minimal changes to the
shared config to remove non-supported features (`alpha` mentions),
- Migrate all apps to be compatible with v4 configs,
- Fix the `typography.css` import in 3 apps,
- Add missing rules which were included by default in v3,
- Run `pnpm dlx @tailwindcss/upgrade` on all apps, which renames a lot
of classes
- Rename all misnamed classes according to
https://tailwindcss.com/docs/upgrade-guide#renamed-utilities in all
apps.

---------

Co-authored-by: Jordi Enric <jordi.err@gmail.com>
2026-04-30 10:53:24 +00:00

409 lines
14 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { useParams } from 'common'
import dayjs from 'dayjs'
import { BarChart2, ChevronRight, ExternalLink, Telescope } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { AiIconAnimation, Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { StatusCode } from 'ui-patterns'
import {
Chart,
ChartActions,
ChartCard,
ChartContent,
ChartEmptyState,
ChartHeader,
ChartLoadingState,
ChartMetric,
ChartTitle,
} from 'ui-patterns/Chart'
import {
PageSection,
PageSectionContent,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import {
AuthErrorCodeRow,
fetchTopAuthErrorCodes,
fetchTopResponseErrors,
ResponseErrorRow,
} from './OverviewErrors.constants'
import { OverviewTable } from './OverviewTable'
import {
AuthMetricsResponse,
calculatePercentageChange,
getApiSuccessRates,
getAuthSuccessRates,
getMetricValues,
} from './OverviewUsage.constants'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import AlertError from '@/components/ui/AlertError'
import { ErrorCodeTooltip } from '@/components/ui/ErrorCodeTooltip/ErrorCodeTooltip'
import { Service } from '@/data/graphql/graphql'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
const StatCard = ({
title,
current,
previous,
loading,
suffix = '',
href,
tooltip,
}: {
title: string
current: number
previous: number
loading: boolean
suffix?: string
invert?: boolean
href?: string
tooltip?: string
}) => {
const router = useRouter()
const formattedCurrent =
suffix === 'ms'
? current.toFixed(2)
: suffix === '%'
? current.toFixed(1)
: Math.round(current).toLocaleString()
// const signChar = previous > 0 ? '+' : previous < 0 ? '-' : ''
const actions = [
{
label: 'Go to Auth Report',
icon: <ExternalLink size={12} />,
onClick: href ? () => router.push(href) : undefined,
},
]
return (
<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>
<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>
</Tooltip>
)
function isResponseErrorRow(row: unknown): row is ResponseErrorRow {
if (!row || typeof row !== 'object') return false
const r = row as Record<string, unknown>
return (
typeof r.method === 'string' &&
typeof r.path === 'string' &&
typeof r.status_code === 'number' &&
typeof r.count === 'number'
)
}
function isAuthErrorCodeRow(row: unknown): row is AuthErrorCodeRow {
if (!row || typeof row !== 'object') return false
const r = row as Record<string, unknown>
return typeof r.error_code === 'string' && typeof r.count === 'number'
}
interface OverviewMetricsProps {
metrics?: AuthMetricsResponse
isLoading: boolean
error: unknown
}
export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsProps) => {
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,
'activeUsers'
)
const { current: signUpsCurrent, previous: signUpsPrevious } = getMetricValues(
metrics,
'signUpCount'
)
const activeUsersChange = calculatePercentageChange(activeUsersCurrent, activeUsersPrevious)
const signUpsChange = calculatePercentageChange(signUpsCurrent, signUpsPrevious)
const { current: apiSuccessRateCurrent, previous: apiSuccessRatePrevious } =
getApiSuccessRates(metrics)
const { current: authSuccessRateCurrent, previous: authSuccessRatePrevious } =
getAuthSuccessRates(metrics)
const apiSuccessRateChange = calculatePercentageChange(
apiSuccessRateCurrent,
apiSuccessRatePrevious
)
const authSuccessRateChange = calculatePercentageChange(
authSuccessRateCurrent,
authSuccessRatePrevious
)
const { data: respErrData, isPending: isLoadingResp } = useQuery({
queryKey: ['auth-overview', ref, 'top-response-errors'],
queryFn: () => fetchTopResponseErrors(ref as string),
enabled: !!ref,
})
const { data: codeErrData, isPending: isLoadingCodes } = useQuery({
queryKey: ['auth-overview', ref, 'top-auth-error-codes'],
queryFn: () => fetchTopAuthErrorCodes(ref as string),
enabled: !!ref,
})
const responseErrors: ResponseErrorRow[] = Array.isArray(respErrData?.result)
? (respErrData?.result as unknown[]).filter(isResponseErrorRow)
: []
const errorCodes: AuthErrorCodeRow[] = Array.isArray(codeErrData?.result)
? (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>
{!!error && (
<AlertError
className="mb-4"
subject="Error fetching auth metrics"
error={{
message: 'There was an error fetching the auth metrics.',
}}
/>
)}
<PageSectionMeta>
<PageSectionSummary>
<div className="flex items-center justify-between">
<PageSectionTitle>Usage</PageSectionTitle>
<Link
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}&isHelper=true&helperText=Last+24+hours`}
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} className="text-foreground-lighter" />
<span>Go to observability</span>
<ChevronRight size={14} className="text-foreground-lighter" />
</Link>
</div>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<StatCard
title="Auth Activity"
current={activeUsersCurrent}
previous={activeUsersChange}
loading={isLoading}
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
tooltip="Users who generated any Auth event in this period. This metric tracks authentication activity, not total product usage. Some active users won't appear here if their session stayed valid."
/>
<StatCard
title="Sign ups"
current={signUpsCurrent}
previous={signUpsChange}
loading={isLoading}
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
/>
</div>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Monitoring</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
<StatCard
title="Auth API Success Rate"
current={apiSuccessRateCurrent}
previous={apiSuccessRateChange}
loading={isLoading}
suffix="%"
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
/>
<StatCard
title="Auth Server Success Rate"
current={authSuccessRateCurrent}
previous={authSuccessRateChange}
loading={isLoading}
suffix="%"
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
/>
</div>
<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) => {
return <StatusCode method={row.method} statusCode={row.status_code} />
},
},
{
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 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>
<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) => (
<ErrorCodeTooltip errorCode={row.error_code} service={Service.Auth}>
<span className="line-clamp-1 font-mono uppercase text-xs inline-flex text-foreground-light cursor-default">
{row.error_code}
</span>
</ErrorCodeTooltip>
),
},
{
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>
</>
)
}