mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore: more dead code cleanup (#47312)
## Problem There's still more unused code in the repository which slows down everything: - checkouts - tooling - probably builds (not sure how good turbopack is at handling this) ## Solution - remove old unused code - remove more recent code after checking git history to ensure it's not unfinished/ongoing work <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Removed several outdated UI components and helper utilities to streamline the app. * Cleaned up unused analytics, database, and observability hooks and queries. * **Refactor** * Simplified data table, unified logs, and assistant panel internals by removing legacy display and navigation pieces. * **Bug Fixes** * Reduced the chance of showing stale or inconsistent status, chart, and metric views by eliminating obsolete display paths. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
b1b29ad011
commit
77bf0a4ec9
24 files changed
-1701
No files matched your search
-81
@@ -1,81 +0,0 @@
|
||||
import { SchemaInstallationStatus } from '@stripe/sync-engine/supabase'
|
||||
import { AlertCircle, Check, RefreshCwIcon } from 'lucide-react'
|
||||
|
||||
import {
|
||||
hasInstallError,
|
||||
hasUninstallError,
|
||||
isInstalled,
|
||||
isInstalling,
|
||||
isUninstalling,
|
||||
} from './stripe-sync-status'
|
||||
|
||||
export const StatusDisplay = ({
|
||||
status,
|
||||
isInstallRequested,
|
||||
isInstallInitiated,
|
||||
isUninstallRequested,
|
||||
isUninstallInitiated,
|
||||
isUpgrade,
|
||||
timedOut,
|
||||
}: {
|
||||
status: SchemaInstallationStatus
|
||||
isInstallRequested: boolean
|
||||
isInstallInitiated: boolean
|
||||
isUninstallRequested: boolean
|
||||
isUninstallInitiated: boolean
|
||||
isUpgrade?: boolean
|
||||
timedOut: boolean
|
||||
}) => {
|
||||
const installed = isInstalled(status)
|
||||
const installError = hasInstallError(status)
|
||||
const uninstallError = hasUninstallError(status)
|
||||
const installInProgress = isInstalling(status)
|
||||
const uninstallInProgress = isUninstalling(status)
|
||||
|
||||
const installing = (installInProgress || isInstallRequested || isInstallInitiated) && !timedOut
|
||||
const uninstalling =
|
||||
(uninstallInProgress || isUninstallRequested || isUninstallInitiated) && !timedOut
|
||||
|
||||
if (uninstalling) {
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-foreground-light text-sm">
|
||||
<RefreshCwIcon size={14} className="animate-spin text-foreground-lighter" />
|
||||
Uninstalling...
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (uninstallError) {
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-foreground-light text-sm">
|
||||
<AlertCircle size={14} className="text-destructive" />
|
||||
Uninstallation error
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (installing) {
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-foreground-light text-sm">
|
||||
<RefreshCwIcon size={14} className="animate-spin text-foreground-lighter" />
|
||||
{isUpgrade ? 'Upgrading...' : 'Installing...'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (installError) {
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-foreground-light text-sm">
|
||||
<AlertCircle size={14} className="text-destructive" />
|
||||
{isUpgrade ? 'Upgrade error' : 'Installation error'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (installed) {
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-foreground-light text-sm">
|
||||
<Check size={14} strokeWidth={1.5} className="text-brand" /> Installed
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-foreground-light text-sm">Not installed</span>
|
||||
)
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import { Button, Card, CardContent, CardFooter, CardHeader, CardTitle, cn, Loading } from 'ui'
|
||||
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
|
||||
|
||||
import type { LogsBarChartDatum } from '../ProjectHome/ProjectUsage.metrics'
|
||||
import { getHealthStatus, type ServiceKey } from './ObservabilityOverview.utils'
|
||||
import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
|
||||
|
||||
const colorClassMap: Record<string, string> = {
|
||||
muted: 'bg-muted',
|
||||
destructive: 'bg-destructive',
|
||||
warning: 'bg-warning',
|
||||
brand: 'bg-brand',
|
||||
}
|
||||
|
||||
export type ServiceHealthCardProps = {
|
||||
serviceName: string
|
||||
serviceKey: ServiceKey
|
||||
total: number
|
||||
errorRate: number
|
||||
errorCount: number
|
||||
warningCount: number
|
||||
chartData: LogsBarChartDatum[]
|
||||
reportUrl?: string // undefined if feature flag disabled or no report available
|
||||
logsUrl: string
|
||||
isLoading: boolean
|
||||
onBarClick: (datum: any) => void
|
||||
datetimeFormat: string
|
||||
}
|
||||
|
||||
export const ServiceHealthCard = ({
|
||||
serviceName,
|
||||
serviceKey: _serviceKey,
|
||||
total,
|
||||
errorRate,
|
||||
errorCount,
|
||||
warningCount,
|
||||
chartData,
|
||||
reportUrl,
|
||||
logsUrl,
|
||||
isLoading,
|
||||
onBarClick,
|
||||
datetimeFormat,
|
||||
}: ServiceHealthCardProps) => {
|
||||
const { color } = getHealthStatus(errorRate, total)
|
||||
|
||||
return (
|
||||
<Card className="mb-0 md:mb-0 h-full flex flex-col">
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-2 space-y-0 pb-0 border-b-0">
|
||||
<div className="flex flex-col">
|
||||
<CardTitle className="text-foreground-light flex items-center gap-2 text-xs font-medium">
|
||||
<div className={cn('w-1.5 h-1.5 rounded-full', colorClassMap[color] || 'bg-muted')} />
|
||||
{serviceName}
|
||||
</CardTitle>
|
||||
<div className="flex items-start gap-6 mt-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-foreground text-xl">{total.toLocaleString()}</span>
|
||||
<span className="text-foreground-light text-xs">requests</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-foreground text-xl">{errorRate.toFixed(1)}%</span>
|
||||
<span className="text-foreground-light text-xs">error rate</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end gap-4 text-foreground-light">
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-warning rounded-full" />
|
||||
<span className="heading-meta">Warn</span>
|
||||
</div>
|
||||
<span className="text-foreground text-xl">{warningCount.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-destructive rounded-full" />
|
||||
<span className="heading-meta">Err</span>
|
||||
</div>
|
||||
<span className="text-foreground text-xl">{errorCount.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-card pt-4 flex-1 overflow-hidden max-h-[200px]">
|
||||
<Loading isFullHeight active={isLoading}>
|
||||
<LogsBarChart
|
||||
isFullHeight
|
||||
data={chartData}
|
||||
DateTimeFormat={datetimeFormat}
|
||||
onBarClick={onBarClick}
|
||||
EmptyState={
|
||||
<NoDataPlaceholder size="small" message="No data for selected period" isFullHeight />
|
||||
}
|
||||
/>
|
||||
</Loading>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="border-t pt-3 pb-4 px-card flex gap-2">
|
||||
{reportUrl && (
|
||||
<Button variant="default" size="tiny" asChild className="flex-1">
|
||||
<Link href={reportUrl}>View Report</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="default" size="tiny" asChild className="flex-1">
|
||||
<Link href={logsUrl}>View Logs</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import type { LogsBarChartDatum } from '../ProjectHome/ProjectUsage.metrics'
|
||||
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
|
||||
import { safeSql, type SafeLogSqlFragment } from '@/data/logs/safe-analytics-sql'
|
||||
|
||||
type PostgrestMetricsVariables = {
|
||||
projectRef: string
|
||||
startDate: string
|
||||
endDate: string
|
||||
interval: '1hr' | '1day' | '7day'
|
||||
}
|
||||
|
||||
function getIntervalTrunc(interval: '1hr' | '1day' | '7day'): SafeLogSqlFragment {
|
||||
switch (interval) {
|
||||
case '1hr':
|
||||
return safeSql`minute` // 1-minute buckets for 1 hour
|
||||
case '1day':
|
||||
return safeSql`hour` // 1-hour buckets for 1 day
|
||||
case '7day':
|
||||
return safeSql`day` // 1-day buckets for 7 days
|
||||
default:
|
||||
return safeSql`hour`
|
||||
}
|
||||
}
|
||||
|
||||
const POSTGREST_METRICS_SQL = (interval: '1hr' | '1day' | '7day'): SafeLogSqlFragment => {
|
||||
const trunc = getIntervalTrunc(interval)
|
||||
|
||||
return safeSql`
|
||||
-- postgrest-overview-metrics
|
||||
select
|
||||
cast(timestamp_trunc(t.timestamp, ${trunc}) as datetime) as timestamp,
|
||||
countif(response.status_code < 300) as ok_count,
|
||||
countif(response.status_code >= 300 and response.status_code < 400) as warning_count,
|
||||
countif(response.status_code >= 400) as error_count
|
||||
FROM edge_logs t
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as response
|
||||
cross join unnest(m.request) as request
|
||||
WHERE
|
||||
request.path like '/rest/%'
|
||||
GROUP BY
|
||||
timestamp
|
||||
ORDER BY
|
||||
timestamp ASC
|
||||
`
|
||||
}
|
||||
|
||||
type MetricsRow = {
|
||||
timestamp: string
|
||||
ok_count: number
|
||||
warning_count: number
|
||||
error_count: number
|
||||
}
|
||||
|
||||
async function fetchPostgrestMetrics(
|
||||
{ projectRef, startDate, endDate, interval }: PostgrestMetricsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const sql = POSTGREST_METRICS_SQL(interval)
|
||||
|
||||
const data = await executeAnalyticsSql({
|
||||
projectRef,
|
||||
endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all',
|
||||
sql,
|
||||
iso_timestamp_start: startDate,
|
||||
iso_timestamp_end: endDate,
|
||||
method: 'get',
|
||||
signal,
|
||||
})
|
||||
|
||||
if (data?.error) throw data.error
|
||||
|
||||
return (data?.result || []) as MetricsRow[]
|
||||
}
|
||||
|
||||
export const usePostgrestOverviewMetrics = (
|
||||
{ projectRef, startDate, endDate, interval }: PostgrestMetricsVariables,
|
||||
options?: { enabled?: boolean }
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ['postgrest-overview-metrics', projectRef, startDate, endDate, interval],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchPostgrestMetrics({ projectRef, startDate, endDate, interval }, signal),
|
||||
enabled: (options?.enabled ?? true) && Boolean(projectRef),
|
||||
staleTime: 1000 * 60,
|
||||
})
|
||||
}
|
||||
|
||||
export const transformPostgrestMetrics = (rows: MetricsRow[]): LogsBarChartDatum[] => {
|
||||
return rows.map((row) => ({
|
||||
timestamp: row.timestamp,
|
||||
ok_count: row.ok_count,
|
||||
warning_count: row.warning_count,
|
||||
error_count: row.error_count,
|
||||
}))
|
||||
}
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
import { Skeleton } from 'ui'
|
||||
|
||||
import { BlockFieldProps } from '../../types'
|
||||
import { TruncatedTextWithPopover } from './TruncatedTextWithPopover'
|
||||
import { DataTableColumnStatusCode } from '@/components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode'
|
||||
import { DataTableSheetRowAction } from '@/components/ui/DataTable/DataTableSheetRowAction'
|
||||
|
||||
export const BlockField = ({
|
||||
config,
|
||||
data,
|
||||
enrichedData,
|
||||
isLoading,
|
||||
filterFields,
|
||||
table,
|
||||
}: BlockFieldProps) => {
|
||||
const value = config.getValue(data, enrichedData)
|
||||
const displayValue = value ?? 'No value'
|
||||
const stringValue = String(displayValue)
|
||||
|
||||
const shouldShowSkeleton = config.requiresEnrichedData && isLoading && !value
|
||||
|
||||
const filterField = filterFields.find((field) => field.value === config.id)
|
||||
const isFilterable = !!filterField
|
||||
|
||||
// Special handling for status field and key fields
|
||||
const isStatusField = config.id === 'status'
|
||||
const isApiKeyField = config.id === 'api_key_role'
|
||||
|
||||
// Truncation config
|
||||
const maxLength = config.maxLength || 50 // Default to 50 characters
|
||||
|
||||
const fieldContent = (
|
||||
<>
|
||||
<dt className="text-[13.5px] text-foreground-light">{config.label}</dt>
|
||||
<dd className="text-right">
|
||||
{shouldShowSkeleton ? (
|
||||
<Skeleton className="h-4 w-24" />
|
||||
) : isStatusField && displayValue !== 'N/A' ? (
|
||||
<DataTableColumnStatusCode value={displayValue} level={data?.level} className="text-xs" />
|
||||
) : isApiKeyField && displayValue !== 'N/A' ? (
|
||||
<TruncatedTextWithPopover
|
||||
text={stringValue}
|
||||
maxLength={maxLength}
|
||||
className="px-2 py-1 text-xs font-mono border border-border rounded-sm bg-surface-100"
|
||||
/>
|
||||
) : displayValue !== 'N/A' ? (
|
||||
<TruncatedTextWithPopover
|
||||
text={stringValue}
|
||||
maxLength={maxLength}
|
||||
className={`text-sm font-mono ${
|
||||
isFilterable ? 'text-foreground cursor-pointer hover:underline' : 'text-foreground'
|
||||
}`}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={`text-sm font-mono ${
|
||||
isFilterable ? 'text-foreground cursor-pointer hover:underline' : 'text-foreground'
|
||||
} text-foreground-light`}
|
||||
>
|
||||
{displayValue}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
</>
|
||||
)
|
||||
|
||||
if (isFilterable && !shouldShowSkeleton && displayValue !== 'N/A') {
|
||||
return (
|
||||
<DataTableSheetRowAction
|
||||
fieldValue={config.id}
|
||||
filterFields={filterFields}
|
||||
value={stringValue}
|
||||
table={table}
|
||||
className="flex justify-between items-center py-1 px-2 rounded-sm hover:bg-accent/50 cursor-pointer w-full hover:bg-surface-400"
|
||||
>
|
||||
{fieldContent}
|
||||
</DataTableSheetRowAction>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className="flex justify-between items-center py-1 px-2">{fieldContent}</div>
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
import { Table } from '@tanstack/react-table'
|
||||
import { X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Badge, Collapsible, CollapsibleContent, CollapsibleTrigger, Skeleton } from 'ui'
|
||||
|
||||
import { BlockFieldConfig } from '../../types'
|
||||
import { BlockField } from './BlockField'
|
||||
import { DataTableFilterField } from '@/components/ui/DataTable/DataTable.types'
|
||||
|
||||
// Single source of truth for field row styling
|
||||
export const FieldRow = ({
|
||||
label,
|
||||
value,
|
||||
expandButton,
|
||||
}: {
|
||||
label: string
|
||||
value: React.ReactNode
|
||||
expandButton?: React.ReactNode
|
||||
}) => (
|
||||
<div className="flex justify-between items-center py-1 px-2">
|
||||
<dt className="flex items-center gap-2 text-[13.5px] text-foreground-light">
|
||||
<span>{label}</span>
|
||||
{expandButton}
|
||||
</dt>
|
||||
<dd className="text-right">{value}</dd>
|
||||
</div>
|
||||
)
|
||||
|
||||
interface FieldWithSeeMoreProps {
|
||||
primaryField: BlockFieldConfig
|
||||
additionalFields: BlockFieldConfig[]
|
||||
data: any
|
||||
enrichedData?: any
|
||||
isLoading?: boolean
|
||||
filterFields: DataTableFilterField<any>[]
|
||||
table: Table<any>
|
||||
showValueAsBadge?: boolean
|
||||
}
|
||||
|
||||
// Primary field with expandable additional details
|
||||
export const FieldWithSeeMore = ({
|
||||
primaryField,
|
||||
additionalFields,
|
||||
data,
|
||||
enrichedData,
|
||||
isLoading,
|
||||
filterFields,
|
||||
table,
|
||||
showValueAsBadge = false,
|
||||
}: FieldWithSeeMoreProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const hasAdditionalData = additionalFields.some(
|
||||
(field) => field.getValue(data, enrichedData) && field.getValue(data, enrichedData) !== 'N/A'
|
||||
)
|
||||
|
||||
const primaryValue = primaryField.getValue(data, enrichedData)
|
||||
const shouldShowSkeleton = primaryField.requiresEnrichedData && isLoading && !primaryValue
|
||||
const isApiKeyField = primaryField.id === 'api_key_role'
|
||||
|
||||
// Common value rendering logic
|
||||
const renderValue = () => {
|
||||
if (shouldShowSkeleton) {
|
||||
return <Skeleton className="h-4 w-24" />
|
||||
}
|
||||
|
||||
if (isApiKeyField && primaryValue && primaryValue !== 'N/A') {
|
||||
return (
|
||||
<span className="border border-border rounded-sm px-2 py-1 bg-surface-100 text-xs font-mono text-foreground">
|
||||
{primaryValue}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (showValueAsBadge && primaryValue && primaryValue !== 'N/A') {
|
||||
return <Badge variant="secondary">{primaryValue}</Badge>
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`text-sm font-mono text-foreground ${
|
||||
primaryValue === 'N/A' ? 'text-foreground-light' : ''
|
||||
}`}
|
||||
>
|
||||
{primaryValue ?? 'N/A'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const expandButton = hasAdditionalData ? (
|
||||
<CollapsibleTrigger asChild>
|
||||
<button className="w-3 h-3 flex items-center justify-center selection:font-mono text-foreground-lighter text-xs hover:text-foreground-light bg-foreground-muted/75 rounded-sm [&[data-state=open]>svg]:rotate-180 hover:bg-foreground-lighter">
|
||||
<X size={10} className="text-background-surface-400 rotate-45" strokeWidth={3} />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
) : undefined
|
||||
|
||||
return (
|
||||
<div className="border-t border-border">
|
||||
{hasAdditionalData ? (
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<FieldRow label={primaryField.label} value={renderValue()} expandButton={expandButton} />
|
||||
<CollapsibleContent className="transition-all data-closed:animate-collapsible-up data-open:animate-collapsible-down">
|
||||
<div className="mt-1">
|
||||
{additionalFields.map((field) => (
|
||||
<BlockField
|
||||
key={field.id}
|
||||
config={field}
|
||||
data={data}
|
||||
enrichedData={enrichedData}
|
||||
isLoading={isLoading}
|
||||
filterFields={filterFields}
|
||||
table={table}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : (
|
||||
<FieldRow label={primaryField.label} value={renderValue()} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from 'ui'
|
||||
|
||||
interface TruncatedTextWithPopoverProps {
|
||||
text: string
|
||||
maxLength?: number
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const TruncatedTextWithPopover = ({
|
||||
text,
|
||||
maxLength = 50,
|
||||
className = '',
|
||||
children,
|
||||
}: TruncatedTextWithPopoverProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const isTruncated = text.length > maxLength
|
||||
const truncatedText = isTruncated ? text.substring(0, maxLength) + '...' : text
|
||||
|
||||
// If not truncated, just render the text without popover
|
||||
if (!isTruncated) {
|
||||
return children ? <>{children}</> : <span className={className}>{text}</span>
|
||||
}
|
||||
|
||||
// If truncated, render with popover
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<span className={`cursor-help ${className}`}>{children || truncatedText}</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="max-w-md p-3 wrap-break-word" side="top">
|
||||
<div className="text-sm font-mono whitespace-pre-wrap">{text}</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import { BlockFieldConfig } from '../types'
|
||||
|
||||
/**
|
||||
* Common field configuration helpers to reduce duplication
|
||||
*/
|
||||
|
||||
// Helper for simple fields that fallback from enrichedData to data
|
||||
export const createField = (
|
||||
id: string,
|
||||
label: string,
|
||||
enrichedKey?: string,
|
||||
dataKey?: string,
|
||||
requiresEnrichedData = false
|
||||
): BlockFieldConfig => ({
|
||||
id,
|
||||
label,
|
||||
getValue: (data, enrichedData) => {
|
||||
const eKey = enrichedKey || id
|
||||
const dKey = dataKey || id
|
||||
return enrichedData?.[eKey] || data?.[dKey]
|
||||
},
|
||||
requiresEnrichedData,
|
||||
})
|
||||
|
||||
// Helper for truncated text fields
|
||||
export const createTruncatedField = (
|
||||
id: string,
|
||||
label: string,
|
||||
maxLength: number,
|
||||
enrichedKey?: string,
|
||||
dataKey?: string,
|
||||
requiresEnrichedData = false
|
||||
): BlockFieldConfig => ({
|
||||
id,
|
||||
label,
|
||||
getValue: (data, enrichedData) => {
|
||||
const eKey = enrichedKey || id
|
||||
const dKey = dataKey || id
|
||||
const value = enrichedData?.[eKey] || data?.[dKey]
|
||||
if (!value || typeof value !== 'string') return value
|
||||
return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value
|
||||
},
|
||||
requiresEnrichedData,
|
||||
})
|
||||
|
||||
// Helper for ID fields that show truncated version
|
||||
export const createIdField = (
|
||||
id: string,
|
||||
label: string,
|
||||
truncateLength = 8,
|
||||
enrichedKey?: string,
|
||||
dataKey?: string,
|
||||
requiresEnrichedData = false
|
||||
): BlockFieldConfig => ({
|
||||
id,
|
||||
label,
|
||||
getValue: (data, enrichedData) => {
|
||||
const eKey = enrichedKey || id
|
||||
const dKey = dataKey || id
|
||||
const value = enrichedData?.[eKey] || data?.[dKey]
|
||||
if (!value) return null
|
||||
const stringValue = String(value)
|
||||
return truncateLength ? `${stringValue.substring(0, truncateLength)}...` : stringValue
|
||||
},
|
||||
requiresEnrichedData,
|
||||
})
|
||||
|
||||
// Helper for time/duration fields
|
||||
export const createTimeField = (
|
||||
id: string,
|
||||
label: string,
|
||||
unit = 'ms',
|
||||
enrichedKey?: string,
|
||||
dataKey?: string,
|
||||
requiresEnrichedData = false
|
||||
): BlockFieldConfig => ({
|
||||
id,
|
||||
label,
|
||||
getValue: (data, enrichedData) => {
|
||||
const eKey = enrichedKey || id
|
||||
const dKey = dataKey || id
|
||||
const time = enrichedData?.[eKey] || data?.[dKey]
|
||||
return time ? `${time}${unit}` : null
|
||||
},
|
||||
requiresEnrichedData,
|
||||
})
|
||||
|
||||
// Helper for status fields with fallback logic
|
||||
export const createStatusField = (
|
||||
id: string = 'status',
|
||||
label: string = 'Status'
|
||||
): BlockFieldConfig => ({
|
||||
id,
|
||||
label,
|
||||
getValue: (data, enrichedData) => enrichedData?.status || data?.status,
|
||||
})
|
||||
|
||||
// Helper for path/filename extraction
|
||||
export const createFileNameField = (
|
||||
id: string,
|
||||
label: string,
|
||||
pathKey = 'path',
|
||||
requiresEnrichedData = false
|
||||
): BlockFieldConfig => ({
|
||||
id,
|
||||
label,
|
||||
getValue: (data, enrichedData) => {
|
||||
const path = enrichedData?.[pathKey] || data?.[pathKey]
|
||||
if (!path) return null
|
||||
// Remove query parameters and hash, get last segment
|
||||
const cleanPath = path.split('?')[0].split('#')[0]
|
||||
const segments = cleanPath.split('/')
|
||||
return segments[segments.length - 1] || null
|
||||
},
|
||||
requiresEnrichedData,
|
||||
})
|
||||
|
||||
// Helper for conditional fields based on error status
|
||||
export const createConditionalField = (
|
||||
id: string,
|
||||
label: string,
|
||||
getValue: (data: any, enrichedData?: any) => any,
|
||||
errorValue: string = 'Unavailable',
|
||||
deletedValue: string = 'Object deleted',
|
||||
requiresEnrichedData = false
|
||||
): BlockFieldConfig => ({
|
||||
id,
|
||||
label,
|
||||
getValue: (data, enrichedData) => {
|
||||
const status = enrichedData?.status || data?.status
|
||||
const numStatus = Number(status)
|
||||
|
||||
if (status === 404 || status === '404') {
|
||||
return deletedValue
|
||||
} else if (numStatus >= 400) {
|
||||
return errorValue
|
||||
}
|
||||
|
||||
return getValue(data, enrichedData)
|
||||
},
|
||||
requiresEnrichedData,
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
import { User } from 'lucide-react'
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from 'ui/src/components/shadcn/ui/hover-card'
|
||||
|
||||
interface AuthUserHoverCardProps {
|
||||
authUser: string
|
||||
}
|
||||
|
||||
export const AuthUserHoverCard = ({ authUser }: AuthUserHoverCardProps) => {
|
||||
if (!authUser) return null
|
||||
|
||||
return (
|
||||
<HoverCard closeDelay={0}>
|
||||
<HoverCardTrigger className="flex items-center justify-center bg-surface-100 border rounded-full">
|
||||
<User size={14} className="text-foreground-lighter" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-80 p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<User size={20} className="text-foreground" />
|
||||
<h4 className="font-medium">User Details</h4>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">ID/Email:</span> {authUser}
|
||||
</div>
|
||||
{/* Additional user details can be added here if available */}
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { useParams } from 'common'
|
||||
import { useMemo } from 'react'
|
||||
import type { CommandOptions, ICommand } from 'ui-patterns/CommandMenu'
|
||||
import { orderSectionFirst, useQuery, useRegisterCommands } from 'ui-patterns/CommandMenu'
|
||||
|
||||
import { COMMAND_MENU_SECTIONS } from '@/components/interfaces/App/CommandMenu/CommandMenu.utils'
|
||||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
|
||||
const QUERY_PERFORMANCE_COMMAND_ID = 'nav-reports-query-performance'
|
||||
|
||||
export function useReportsGotoCommands(options?: CommandOptions) {
|
||||
let { ref } = useParams()
|
||||
ref ||= '_'
|
||||
|
||||
const { reportsAll } = useIsFeatureEnabled(['reports:all'])
|
||||
|
||||
const commandQuery = useQuery()?.toLowerCase() ?? ''
|
||||
const prioritizeQueryPerformance = commandQuery.includes('query')
|
||||
|
||||
const orderQueryPerformanceCommand = useMemo(() => {
|
||||
if (!prioritizeQueryPerformance) return undefined
|
||||
|
||||
return (existingCommands: ICommand[], incomingCommands: ICommand[]) => {
|
||||
const filteredExisting = existingCommands.filter(
|
||||
(command) => command.id !== QUERY_PERFORMANCE_COMMAND_ID
|
||||
)
|
||||
|
||||
return [...incomingCommands, ...filteredExisting]
|
||||
}
|
||||
}, [prioritizeQueryPerformance])
|
||||
|
||||
const orderNavigateSection = useMemo<CommandOptions['orderSection'] | undefined>(() => {
|
||||
return prioritizeQueryPerformance ? orderSectionFirst : options?.orderSection
|
||||
}, [options?.orderSection, prioritizeQueryPerformance])
|
||||
|
||||
useRegisterCommands(
|
||||
COMMAND_MENU_SECTIONS.NAVIGATE,
|
||||
reportsAll
|
||||
? [
|
||||
{
|
||||
id: 'nav-reports',
|
||||
name: 'Reports',
|
||||
route: `/project/${ref}/observability`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-api',
|
||||
name: 'API Reports',
|
||||
route: `/project/${ref}/observability/api-overview`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-storage',
|
||||
name: 'Storage Reports',
|
||||
route: `/project/${ref}/observability/storage`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-database',
|
||||
name: 'Database Reports',
|
||||
route: `/project/${ref}/observability/database`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
{
|
||||
...options,
|
||||
orderSection: orderNavigateSection,
|
||||
deps: [ref, orderNavigateSection, ...(options?.deps ?? [])],
|
||||
}
|
||||
)
|
||||
|
||||
useRegisterCommands(
|
||||
COMMAND_MENU_SECTIONS.NAVIGATE,
|
||||
reportsAll
|
||||
? [
|
||||
{
|
||||
id: QUERY_PERFORMANCE_COMMAND_ID,
|
||||
name: 'Query Performance Reports',
|
||||
route: `/project/${ref}/observability/query-performance`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
{
|
||||
...options,
|
||||
orderCommands: orderQueryPerformanceCommand ?? options?.orderCommands,
|
||||
orderSection: orderNavigateSection,
|
||||
deps: [ref, orderQueryPerformanceCommand, orderNavigateSection, ...(options?.deps ?? [])],
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface UseAutoScrollProps {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function useAutoScroll({ enabled = true }: UseAutoScrollProps = {}) {
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null)
|
||||
const [isSticky, setIsSticky] = useState(true)
|
||||
const isStickyRef = useRef(true)
|
||||
const lastScrollHeightRef = useRef<number>(null)
|
||||
|
||||
const ref = useCallback((element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
setContainer(element)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const scrollToEnd = useCallback(() => {
|
||||
if (container) {
|
||||
isStickyRef.current = true
|
||||
setIsSticky(true)
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
}, [container])
|
||||
|
||||
useEffect(() => {
|
||||
if (!container || !enabled) return
|
||||
|
||||
let timeoutId: NodeJS.Timeout
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => {
|
||||
if (
|
||||
lastScrollHeightRef.current != null &&
|
||||
container.scrollHeight !== lastScrollHeightRef.current
|
||||
) {
|
||||
lastScrollHeightRef.current = container.scrollHeight
|
||||
if (isStickyRef.current) {
|
||||
scrollToEnd()
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const handleScroll = () => {
|
||||
const isAtBottom =
|
||||
Math.abs(container.scrollHeight - container.scrollTop - container.clientHeight) < 10
|
||||
|
||||
isStickyRef.current = isAtBottom
|
||||
setIsSticky(isAtBottom)
|
||||
}
|
||||
|
||||
// Observe all children of the container
|
||||
Array.from(container.children).forEach((child) => {
|
||||
resizeObserver.observe(child)
|
||||
})
|
||||
|
||||
container.addEventListener('scroll', handleScroll)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId)
|
||||
resizeObserver.disconnect()
|
||||
container.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [container, enabled, scrollToEnd])
|
||||
|
||||
return { ref, isSticky, scrollToEnd, setIsSticky }
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { Activity, BarChartIcon, Loader2 } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren, useMemo, useState } from 'react'
|
||||
import { Button, Tooltip, TooltipContent, TooltipTrigger, WarningIcon } from 'ui'
|
||||
|
||||
import type { ChartData } from './Charts.types'
|
||||
import AreaChart from '@/components/ui/Charts/AreaChart'
|
||||
import BarChart from '@/components/ui/Charts/BarChart'
|
||||
import { AnalyticsInterval } from '@/data/analytics/constants'
|
||||
import { mapMultiResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries'
|
||||
import {
|
||||
InfraMonitoringAttribute,
|
||||
useInfraMonitoringAttributesQuery,
|
||||
} from '@/data/analytics/infra-monitoring-query'
|
||||
import {
|
||||
ProjectDailyStatsAttribute,
|
||||
useProjectDailyStatsQuery,
|
||||
} from '@/data/analytics/project-daily-stats-query'
|
||||
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
|
||||
|
||||
interface ChartHandlerProps {
|
||||
id?: string
|
||||
label: string
|
||||
attribute: string
|
||||
provider: 'infra-monitoring' | 'daily-stats'
|
||||
startDate: string
|
||||
endDate: string
|
||||
interval: string
|
||||
customDateFormat?: string
|
||||
defaultChartStyle?: 'bar' | 'line'
|
||||
hideChartType?: boolean
|
||||
data?: ChartData
|
||||
isLoading?: boolean
|
||||
format?: string
|
||||
highlightedValue?: string | number
|
||||
syncId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls chart display state. Optionally fetches static chart data if data is not provided.
|
||||
*
|
||||
* If the `data` prop is provided, it will disable automatic chart data fetching and pass the data directly to the chart render.
|
||||
* - loading state can also be provided through the `isLoading` prop, to display loading placeholders. Ignored if `data` key not provided.
|
||||
* - if `isLoading=true` and `data` is `undefined`, loading error message will be shown.
|
||||
*
|
||||
* Provided data must be in the expected chart format.
|
||||
*/
|
||||
const ChartHandler = ({
|
||||
label,
|
||||
attribute,
|
||||
provider,
|
||||
startDate,
|
||||
endDate,
|
||||
interval,
|
||||
customDateFormat,
|
||||
children = null,
|
||||
defaultChartStyle = 'bar',
|
||||
hideChartType = false,
|
||||
data,
|
||||
isLoading,
|
||||
format,
|
||||
highlightedValue,
|
||||
syncId,
|
||||
...otherProps
|
||||
}: PropsWithChildren<ChartHandlerProps>) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle)
|
||||
|
||||
const databaseIdentifier = state.selectedDatabaseId
|
||||
|
||||
const { data: dailyStatsData, isPending: isFetchingDailyStats } = useProjectDailyStatsQuery(
|
||||
{
|
||||
projectRef: ref as string,
|
||||
attribute: attribute as ProjectDailyStatsAttribute,
|
||||
startDate: dayjs(startDate).format('YYYY-MM-DD'),
|
||||
endDate: dayjs(endDate).format('YYYY-MM-DD'),
|
||||
},
|
||||
{ enabled: provider === 'daily-stats' && data === undefined }
|
||||
)
|
||||
|
||||
const { data: infraMonitoringData, isPending: isFetchingInfraMonitoring } =
|
||||
useInfraMonitoringAttributesQuery(
|
||||
{
|
||||
projectRef: ref as string,
|
||||
attributes: [attribute as InfraMonitoringAttribute],
|
||||
startDate,
|
||||
endDate,
|
||||
interval: interval as AnalyticsInterval,
|
||||
databaseIdentifier,
|
||||
},
|
||||
{ enabled: provider === 'infra-monitoring' && data === undefined }
|
||||
)
|
||||
|
||||
const transformedInfraData = useMemo(() => {
|
||||
if (!infraMonitoringData) return undefined
|
||||
const mapped = mapMultiResponseToAnalyticsData(infraMonitoringData, [
|
||||
attribute as InfraMonitoringAttribute,
|
||||
])
|
||||
return mapped[attribute]
|
||||
}, [infraMonitoringData, attribute])
|
||||
|
||||
const chartData =
|
||||
data ||
|
||||
(provider === 'infra-monitoring'
|
||||
? transformedInfraData
|
||||
: provider === 'daily-stats'
|
||||
? dailyStatsData
|
||||
: undefined)
|
||||
|
||||
const loading =
|
||||
isLoading ||
|
||||
(provider === 'infra-monitoring'
|
||||
? isFetchingInfraMonitoring
|
||||
: provider === 'daily-stats'
|
||||
? isFetchingDailyStats
|
||||
: isLoading)
|
||||
|
||||
const shouldHighlightMaxValue =
|
||||
provider === 'daily-stats' &&
|
||||
!attribute.includes('ingress') &&
|
||||
!attribute.includes('egress') &&
|
||||
chartData !== undefined &&
|
||||
'maximum' in chartData
|
||||
const shouldHighlightTotalGroupedValue = chartData !== undefined && 'totalGrouped' in chartData
|
||||
|
||||
const _highlightedValue =
|
||||
highlightedValue !== undefined
|
||||
? highlightedValue
|
||||
: shouldHighlightMaxValue
|
||||
? chartData?.maximum
|
||||
: provider === 'daily-stats'
|
||||
? chartData?.total
|
||||
: shouldHighlightTotalGroupedValue
|
||||
? chartData?.totalGrouped?.[attribute]
|
||||
: (chartData?.data[chartData?.data.length - 1] as any)?.[attribute as any]
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-52 w-full flex-col items-center justify-center gap-y-2">
|
||||
<Loader2 size={18} className="animate-spin text-border-strong" />
|
||||
<p className="text-xs text-foreground-lighter">Loading data for {label}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (chartData === undefined) {
|
||||
return (
|
||||
<div className="flex h-64 w-full flex-col items-center justify-center gap-y-2 border border-dashed rounded-md">
|
||||
<WarningIcon />
|
||||
<p className="text-xs text-foreground-lighter">Unable to load data for {label}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="absolute right-6 z-10 flex justify-between">
|
||||
{!hideChartType && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="px-1.5"
|
||||
icon={chartStyle === 'bar' ? <Activity /> : <BarChartIcon />}
|
||||
onClick={() => setChartStyle(chartStyle === 'bar' ? 'line' : 'bar')}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" align="center">
|
||||
View as {chartStyle === 'bar' ? 'line chart' : 'bar chart'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
{chartStyle === 'bar' ? (
|
||||
<BarChart
|
||||
data={(chartData?.data ?? []) as any}
|
||||
format={format || chartData?.format}
|
||||
xAxisKey={'period_start'}
|
||||
yAxisKey={attribute}
|
||||
highlightedValue={_highlightedValue}
|
||||
title={label}
|
||||
customDateFormat={customDateFormat}
|
||||
syncId={syncId}
|
||||
{...otherProps}
|
||||
/>
|
||||
) : (
|
||||
<AreaChart
|
||||
data={(chartData?.data ?? []) as any}
|
||||
format={format || chartData?.format}
|
||||
xAxisKey="period_start"
|
||||
yAxisKey={attribute}
|
||||
highlightedValue={_highlightedValue}
|
||||
title={label}
|
||||
customDateFormat={customDateFormat}
|
||||
syncId={syncId}
|
||||
{...otherProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChartHandler
|
||||
@@ -1,23 +0,0 @@
|
||||
import { type Column } from '@tanstack/react-table'
|
||||
import { TanStackTableHeadSort } from 'ui-patterns/Table'
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue> {
|
||||
column: Column<TData, TValue>
|
||||
title: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `TanStackTableHeadSort` from `ui-patterns/Table` instead.
|
||||
*/
|
||||
export const DataTableColumnHeader = <TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
className,
|
||||
}: DataTableColumnHeaderProps<TData, TValue>) => {
|
||||
return (
|
||||
<TanStackTableHeadSort column={column} className={className}>
|
||||
{title}
|
||||
</TanStackTableHeadSort>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { forwardRef, PropsWithChildren, useEffect, useRef } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export const DataTableHeaderLayout = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<{
|
||||
setTopBarHeight: (height: number) => void
|
||||
}>
|
||||
>(({ setTopBarHeight, ...props }, _ref) => {
|
||||
const topBarRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
const rect = topBarRef.current?.getBoundingClientRect()
|
||||
if (rect) {
|
||||
setTopBarHeight(rect.height)
|
||||
}
|
||||
})
|
||||
|
||||
const topBar = topBarRef.current
|
||||
if (!topBar) return
|
||||
|
||||
observer.observe(topBar)
|
||||
return () => observer.unobserve(topBar)
|
||||
}, [topBarRef])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={topBarRef}
|
||||
className={cn('flex flex-col gap-4 bg-background p-2', 'top-0 z-10 pb-4')}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
DataTableHeaderLayout.displayName = 'DataTableHeaderLayout'
|
||||
@@ -1,123 +0,0 @@
|
||||
import { ChevronDown, ChevronUp, X } from 'lucide-react'
|
||||
import { ReactNode, useCallback, useEffect, useMemo } from 'react'
|
||||
import { Button, cn, Separator, Skeleton, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
|
||||
import { Kbd } from './primitives/Kbd'
|
||||
import { useDataTable } from './providers/DataTableProvider'
|
||||
|
||||
export interface DataTableSheetDetailsProps {
|
||||
title?: ReactNode
|
||||
titleClassName?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export function DataTableSheetDetails({
|
||||
title,
|
||||
titleClassName,
|
||||
children,
|
||||
}: DataTableSheetDetailsProps) {
|
||||
const { table, rowSelection, isLoading } = useDataTable()
|
||||
|
||||
const selectedRowKey = Object.keys(rowSelection)?.[0]
|
||||
|
||||
const selectedRow = useMemo(() => {
|
||||
if (isLoading && !selectedRowKey) return
|
||||
return table.getRowModel().rows.find((row) => row.id === selectedRowKey)
|
||||
}, [selectedRowKey, isLoading, table])
|
||||
|
||||
const index = table.getRowModel().rows.findIndex((row) => row.id === selectedRow?.id)
|
||||
|
||||
const nextId = useMemo(() => table.getRowModel().rows[index + 1]?.id, [index, table])
|
||||
|
||||
const prevId = useMemo(() => table.getRowModel().rows[index - 1]?.id, [index, table])
|
||||
|
||||
const onPrev = useCallback(() => {
|
||||
if (prevId) table.setRowSelection({ [prevId]: true })
|
||||
}, [prevId, table])
|
||||
|
||||
const onNext = useCallback(() => {
|
||||
if (nextId) table.setRowSelection({ [nextId]: true })
|
||||
}, [nextId, table])
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (!selectedRowKey) return
|
||||
|
||||
// REMINDER: prevent dropdown navigation inside of sheet to change row selection
|
||||
const activeElement = document.activeElement
|
||||
const isMenuActive = activeElement?.closest('[role="menu"]')
|
||||
|
||||
if (isMenuActive) return
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
onPrev()
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', down)
|
||||
return () => document.removeEventListener('keydown', down)
|
||||
}, [selectedRowKey, onNext, onPrev])
|
||||
|
||||
return (
|
||||
<div className="relative bg-sidebar">
|
||||
<div className="flex items-center justify-between gap-2 pl-5 pr-2 py-1">
|
||||
<h5 className={cn(titleClassName, 'truncate text-left')}>
|
||||
{isLoading && !selectedRowKey ? <Skeleton className="h-7 w-36" /> : title}
|
||||
</h5>
|
||||
<div className="flex h-7 items-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="text"
|
||||
disabled={!prevId}
|
||||
onClick={onPrev}
|
||||
className="px-1"
|
||||
icon={<ChevronUp />}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Navigate <Kbd>↑</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="text"
|
||||
disabled={!nextId}
|
||||
onClick={onNext}
|
||||
className="px-1"
|
||||
icon={<ChevronDown />}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Navigate <Kbd>↓</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Separator orientation="vertical" className="mx-1" />
|
||||
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="text"
|
||||
onClick={() => table.resetRowSelection()}
|
||||
className="px-1"
|
||||
icon={<X />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
|
||||
import { formatCompactNumber } from './DataTable.utils'
|
||||
import { DataTableFilterControlsDrawer } from './DataTableFilters/DataTableFilterControlsDrawer'
|
||||
import { DataTableResetButton } from './DataTableResetButton'
|
||||
import { DataTableViewOptions } from './DataTableViewOptions'
|
||||
import { Kbd } from './primitives/Kbd'
|
||||
import { useDataTable } from './providers/DataTableProvider'
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
||||
|
||||
interface DataTableToolbarProps {
|
||||
renderActions?: () => ReactNode
|
||||
isFilterBarOpen: boolean
|
||||
setIsFilterBarOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export function DataTableToolbar({
|
||||
renderActions,
|
||||
isFilterBarOpen,
|
||||
setIsFilterBarOpen,
|
||||
}: DataTableToolbarProps) {
|
||||
const { table, isLoading, columnFilters } = useDataTable()
|
||||
const filters = table.getState().columnFilters
|
||||
|
||||
useShortcut(SHORTCUT_IDS.DATA_TABLE_TOGGLE_FILTERS, () => setIsFilterBarOpen((prev) => !prev))
|
||||
|
||||
const rows = useMemo(
|
||||
() => ({
|
||||
total: table.getCoreRowModel().rows.length,
|
||||
filtered: table.getFilteredRowModel().rows.length,
|
||||
}),
|
||||
[isLoading, columnFilters]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="default"
|
||||
icon={isFilterBarOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||
onClick={() => setIsFilterBarOpen((prev) => !prev)}
|
||||
className="hidden sm:flex"
|
||||
>
|
||||
<span className="hidden md:block">{isFilterBarOpen ? 'Hide' : 'Show'} Controls</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
Toggle controls with{' '}
|
||||
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>B</span>
|
||||
</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="block sm:hidden">
|
||||
<DataTableFilterControlsDrawer />
|
||||
</div>
|
||||
<div>
|
||||
<p className="hidden text-sm text-muted-foreground sm:block">
|
||||
<span className="font-mono font-medium">{formatCompactNumber(rows.filtered)}</span> of{' '}
|
||||
<span className="font-mono font-medium">{formatCompactNumber(rows.total)}</span> row(s){' '}
|
||||
<span className="sr-only sm:not-sr-only">filtered</span>
|
||||
</p>
|
||||
<p className="block text-sm text-muted-foreground sm:hidden">
|
||||
<span className="font-mono font-medium">{formatCompactNumber(rows.filtered)}</span>{' '}
|
||||
row(s)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{filters.length ? <DataTableResetButton /> : null}
|
||||
{renderActions?.()}
|
||||
<DataTableViewOptions />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { TableCell, TableRow } from 'ui'
|
||||
|
||||
export function LiveRow({ colSpan }: { colSpan: number }) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className="relative w-(--header-level-size) min-w-(--header-level-size) max-w-(--header-level-size) border-b border-r border-t border-info">
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="absolute inline-flex h-2.5 w-2.5 animate-ping rounded-full bg-brand opacity-75"></span>
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-brand"></span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell colSpan={colSpan} className="border-b border-r border-t text-foreground">
|
||||
Live Mode
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
export const StringNumberOrNull = z
|
||||
.string()
|
||||
.transform((v) => (v === '' ? null : v))
|
||||
.nullable()
|
||||
.refine((value) => value === null || !isNaN(Number(value)), {
|
||||
message: 'Invalid number',
|
||||
})
|
||||
.transform((value) => (value === null ? null : Number(value)))
|
||||
|
||||
/**
|
||||
* [Joshen] After wrangling with RHF I think this is the easiest way to handle nullable number fields
|
||||
* - Declare the field normally as you would in the zod form schema (e.g field: z.number().nullable())
|
||||
* - In the InputField, add a form.register call `{...form.register('field_name', { setValueAs: setValueAsNullableNumber })}`
|
||||
*/
|
||||
export const setValueAsNullableNumber = (v: any) => (v === '' || v === null ? null : parseInt(v))
|
||||
@@ -1,70 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { operations } from 'api-types'
|
||||
|
||||
import { analyticsKeys } from './keys'
|
||||
import { get, handleError } from '@/data/fetchers'
|
||||
import { UseCustomQueryOptions } from '@/types'
|
||||
|
||||
export type FunctionsReqStatsVariables = {
|
||||
projectRef?: string
|
||||
functionId?: string
|
||||
interval?: operations['FunctionsLogsController_getRequestStats']['parameters']['query']['interval']
|
||||
}
|
||||
|
||||
export type FunctionsReqStatsResponse = any
|
||||
|
||||
export async function getFunctionsReqStats(
|
||||
{ projectRef, functionId, interval }: FunctionsReqStatsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
if (!functionId) {
|
||||
throw new Error('functionId is required')
|
||||
}
|
||||
if (!interval) {
|
||||
throw new Error('interval is required')
|
||||
}
|
||||
|
||||
const { data, error } = await get(
|
||||
'/platform/projects/{ref}/analytics/endpoints/functions.req-stats',
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
ref: projectRef,
|
||||
},
|
||||
query: {
|
||||
function_id: functionId,
|
||||
interval,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
}
|
||||
)
|
||||
|
||||
if (error) handleError(error)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export type FunctionsReqStatsData = Awaited<ReturnType<typeof getFunctionsReqStats>>
|
||||
export type FunctionsReqStatsError = unknown
|
||||
|
||||
export const useFunctionsReqStatsQuery = <TData = FunctionsReqStatsData>(
|
||||
{ projectRef, functionId, interval }: FunctionsReqStatsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseCustomQueryOptions<FunctionsReqStatsData, FunctionsReqStatsError, TData> = {}
|
||||
) =>
|
||||
useQuery<FunctionsReqStatsData, FunctionsReqStatsError, TData>({
|
||||
queryKey: analyticsKeys.functionsReqStats(projectRef, { functionId, interval }),
|
||||
queryFn: ({ signal }) => getFunctionsReqStats({ projectRef, functionId, interval }, signal),
|
||||
enabled:
|
||||
enabled &&
|
||||
typeof projectRef !== 'undefined' &&
|
||||
typeof functionId !== 'undefined' &&
|
||||
typeof interval !== 'undefined',
|
||||
...options,
|
||||
})
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { operations } from 'api-types'
|
||||
|
||||
import { analyticsKeys } from './keys'
|
||||
import { get, handleError } from '@/data/fetchers'
|
||||
import { UseCustomQueryOptions } from '@/types'
|
||||
|
||||
export type FunctionsResourceUsageVariables = {
|
||||
projectRef?: string
|
||||
functionId?: string
|
||||
interval?: operations['FunctionsLogsController_getRequestStats']['parameters']['query']['interval']
|
||||
}
|
||||
|
||||
export type FunctionsResourceUsageResponse = any
|
||||
|
||||
export async function getFunctionsResourceUsage(
|
||||
{ projectRef, functionId, interval }: FunctionsResourceUsageVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
if (!functionId) {
|
||||
throw new Error('functionId is required')
|
||||
}
|
||||
if (!interval) {
|
||||
throw new Error('interval is required')
|
||||
}
|
||||
|
||||
const { data, error } = await get(
|
||||
'/platform/projects/{ref}/analytics/endpoints/functions.resource-usage',
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
ref: projectRef,
|
||||
},
|
||||
query: {
|
||||
function_id: functionId,
|
||||
interval,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
}
|
||||
)
|
||||
|
||||
if (error) handleError(error)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export type FunctionsResourceUsageData = Awaited<ReturnType<typeof getFunctionsResourceUsage>>
|
||||
export type FunctionsResourceUsageError = unknown
|
||||
|
||||
export const useFunctionsResourceUsageQuery = <TData = FunctionsResourceUsageData>(
|
||||
{ projectRef, functionId, interval }: FunctionsResourceUsageVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseCustomQueryOptions<FunctionsResourceUsageData, FunctionsResourceUsageError, TData> = {}
|
||||
) =>
|
||||
useQuery<FunctionsResourceUsageData, FunctionsResourceUsageError, TData>({
|
||||
queryKey: analyticsKeys.functionsResourceUsage(projectRef, { functionId, interval }),
|
||||
queryFn: ({ signal }) =>
|
||||
getFunctionsResourceUsage({ projectRef, functionId, interval }, signal),
|
||||
enabled:
|
||||
enabled &&
|
||||
typeof projectRef !== 'undefined' &&
|
||||
typeof functionId !== 'undefined' &&
|
||||
typeof interval !== 'undefined',
|
||||
...options,
|
||||
})
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { apiKeysKeys } from './keys'
|
||||
import { handleError, patch } from '@/data/fetchers'
|
||||
import type { ResponseError, UseCustomMutationOptions } from '@/types'
|
||||
|
||||
export interface UpdateAPIKeybyIdVariables {
|
||||
projectRef?: string
|
||||
id?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export async function updateAPIKeysById(
|
||||
{ projectRef, id, description }: UpdateAPIKeybyIdVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (typeof projectRef === 'undefined') throw new Error('projectRef is required')
|
||||
if (typeof id === 'undefined') throw new Error('API key ID is required')
|
||||
|
||||
const { data, error } = await patch('/v1/projects/{ref}/api-keys/{id}', {
|
||||
params: {
|
||||
path: { ref: projectRef, id },
|
||||
query: { reveal: false },
|
||||
},
|
||||
body: {
|
||||
description: description,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
handleError(error)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export type ResourceUpdateData = Awaited<ReturnType<typeof updateAPIKeysById>>
|
||||
|
||||
export const useResourceUpdateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseCustomMutationOptions<ResourceUpdateData, ResponseError, UpdateAPIKeybyIdVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<ResourceUpdateData, ResponseError, UpdateAPIKeybyIdVariables>({
|
||||
mutationFn: (vars) => updateAPIKeysById(vars),
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: apiKeysKeys.list(projectRef) })
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to mutate: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// https://www.postgresql.org/docs/current/catalog-pg-constraint.html
|
||||
export enum FOREIGN_KEY_CASCADE_ACTION {
|
||||
NO_ACTION = 'a',
|
||||
RESTRICT = 'r',
|
||||
CASCADE = 'c',
|
||||
SET_NULL = 'n',
|
||||
SET_DEFAULT = 'd',
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { getEntityDefinitionsSql } from '@supabase/pg-meta'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { databaseKeys } from './keys'
|
||||
import { executeSql } from '@/data/sql/execute-sql-mutation'
|
||||
import { ResponseError, UseCustomQueryOptions } from '@/types'
|
||||
|
||||
export type EntityDefinitionsVariables = {
|
||||
limit?: number
|
||||
projectRef?: string
|
||||
connectionString?: string | null
|
||||
schemas: string[]
|
||||
}
|
||||
|
||||
export async function getEntityDefinitions(
|
||||
{ projectRef, connectionString, schemas, limit }: EntityDefinitionsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const sql = getEntityDefinitionsSql({ schemas, limit })
|
||||
const { result } = await executeSql(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql,
|
||||
queryKey: ['entity-definitions', schemas],
|
||||
},
|
||||
signal
|
||||
)
|
||||
|
||||
return result[0].data.definitions
|
||||
}
|
||||
|
||||
type EntityDefinition = { id: number; sql: string }
|
||||
export type EntityDefinitionsData = EntityDefinition[]
|
||||
export type EntityDefinitionsError = ResponseError
|
||||
|
||||
export const useEntityDefinitionsQuery = <TData = EntityDefinitionsData>(
|
||||
{ projectRef, connectionString, schemas, limit }: EntityDefinitionsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseCustomQueryOptions<EntityDefinitionsData, EntityDefinitionsError, TData> = {}
|
||||
) =>
|
||||
useQuery<EntityDefinitionsData, EntityDefinitionsError, TData>({
|
||||
queryKey: databaseKeys.entityDefinitions(projectRef, schemas),
|
||||
queryFn: ({ signal }) =>
|
||||
getEntityDefinitions({ projectRef, connectionString, schemas, limit }, signal),
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && schemas.length > 0,
|
||||
...options,
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { databaseKeys } from './keys'
|
||||
import { get, handleError } from '@/data/fetchers'
|
||||
import type { ResponseError, UseCustomQueryOptions } from '@/types'
|
||||
|
||||
export type PgbouncerStatusVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export async function getPgbouncerStatus(
|
||||
{ projectRef }: PgbouncerStatusVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
const { data, error } = await get('/platform/projects/{ref}/config/pgbouncer/status', {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
export type PgbouncerStatusData = Awaited<ReturnType<typeof getPgbouncerStatus>>
|
||||
export type PgbouncerStatusError = ResponseError
|
||||
|
||||
export const usePgbouncerStatusQuery = <TData = PgbouncerStatusData>(
|
||||
{ projectRef }: PgbouncerStatusVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseCustomQueryOptions<PgbouncerStatusData, PgbouncerStatusError, TData> = {}
|
||||
) =>
|
||||
useQuery<PgbouncerStatusData, PgbouncerStatusError, TData>({
|
||||
queryKey: databaseKeys.pgbouncerStatus(projectRef),
|
||||
queryFn: ({ signal }) => getPgbouncerStatus({ projectRef }, signal),
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
/* Utils for use in queries / mutations */
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 100
|
||||
|
||||
export function getPagination(page?: number, size: number = DEFAULT_PAGE_SIZE) {
|
||||
const limit = size
|
||||
const from = page ? page * limit : 0
|
||||
const to = page ? from + size - 1 : size - 1
|
||||
|
||||
return { from, to }
|
||||
}
|
||||
Reference in new issue
Block a user