Files
supabase/apps/studio/pages/project/[ref]/observability/database.tsx
T
Alaister YoungandAlaister Young b0e31be89a chore(studio): remove dead code found by knip (#49719)
Removes Studio code that nothing imports, as reported by knip. First PR
in a stack of three: this one is pure deletions, #49720 removes the
unused dependencies, #49721 upgrades knip and adds the CI gate so this
doesn't accumulate again.

Every file was verified with a repo-wide grep for its basename, exported
symbols, and string/dynamic imports before deletion — none are reachable
via `next/dynamic`, a barrel file, or a config.

**Removed:**
-
`Billing/Usage/UsageWarningAlerts/{CPU,RAM,DiskIOBandwidth}Warnings.tsx`
(whole directory)
- `DataWarehouse/FormFooterChangeBadge.tsx` (whole directory)
- `Database/Replication/ReplicationDiagram/EmptyReplicationDiagram.tsx`
- `Integrations/Vercel/OrganizationPicker.tsx`
- `QueryInsights/QueryInsightsTable/QueryInsightsTableRow.tsx`
- `hooks/misc/useTrackExperimentExposure.ts`
- `data/ai/{parse-client-code,sql-policy}-mutation.ts`,
`data/misc/parse-query-mutation.ts`,
`data/database/table-check-rls-mutation.ts`
-
`data/notifications/notifications-v2-{archive-all-mutation,summary-query}.ts`
+ their two now-unused keys in `notifications/keys.ts` (`listV2` kept)
-
`data/platform-apps/platform-app-{update,signing-key-delete}-mutation.ts`
- `DateTimeFormats.DATE_ONLY` and the unused
`Notebooks.{MarkdownCell,LogCell,ChartConfig}` types

**Changed:**
- `ReportPadding` no longer has a duplicate default export; its 9
default importers (observability pages) now use the named export

Not removed: `CONSTRAINT_TYPE`'s unused members mirror the closed set of
`pg_constraint.contype` values, so they're documentation rather than
dead code — suppressed narrowly in #49721's knip config instead.

## To test

- `pnpm --filter studio run typecheck` and `lint:ratchet` pass
- Observability pages (`/project/[ref]/observability/*`) still render
with padding — they're the only code touched, via the `ReportPadding`
import change
- Notifications popover still loads and marks-as-read (the removed keys
weren't used for invalidation)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Removed Features**
  - Removed CPU, memory, and disk usage warning alerts.
- Removed the Vercel organization picker and empty replication diagram.
  - Removed query insights row actions and several SQL assistance tools.
  - Removed notification summary and archive-all capabilities.
  - Removed platform app update and signing-key deletion actions.
- Removed the form change-count badge and experiment exposure tracking.

- **Refactor**
- Updated observability reports to use the revised report layout export.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-08-31 10:55:23 +08:00

485 lines
19 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { useFlag, useParams } from 'common'
import dayjs from 'dayjs'
import { ArrowRight, ExternalLink, RefreshCw } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { Alert, AlertDescription, Button } from 'ui'
import { OBSERVABILITY_DOCS_HREFS } from '@/components/interfaces/Observability/Observability.constants'
import ReportHeader from '@/components/interfaces/Reports/ReportHeader'
import { ReportPadding } from '@/components/interfaces/Reports/ReportPadding'
import { REPORT_DATERANGE_HELPER_LABELS } from '@/components/interfaces/Reports/Reports.constants'
import ReportStickyNav from '@/components/interfaces/Reports/ReportStickyNav'
import ReportWidget from '@/components/interfaces/Reports/ReportWidget'
import { ReportChartUpsell } from '@/components/interfaces/Reports/v2/ReportChartUpsell'
import { POOLING_OPTIMIZATIONS } from '@/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.constants'
import DiskSizeConfigurationModal from '@/components/interfaces/Settings/Database/DiskSizeConfigurationModal'
import { getInfrastructurePath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils'
import { LogsDatePicker } from '@/components/interfaces/Settings/Logs/Logs.DatePickers'
import UpgradePrompt from '@/components/interfaces/Settings/Logs/UpgradePrompt'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import ObservabilityLayout from '@/components/layouts/ObservabilityLayout/ObservabilityLayout'
import Table from '@/components/to-be-cleaned/Table'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import type { MultiAttribute } from '@/components/ui/Charts/ComposedChart.utils'
import { LazyComposedChartHandler } from '@/components/ui/Charts/ComposedChartHandler'
import { ReportSettings } from '@/components/ui/Charts/ReportSettings'
import { DocsButton } from '@/components/ui/DocsButton'
import { ObservabilityLink } from '@/components/ui/ObservabilityLink'
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
import { analyticsKeys } from '@/data/analytics/keys'
import { useDiskAttributesQuery } from '@/data/config/disk-attributes-query'
import { useProjectDiskResizeMutation } from '@/data/config/project-disk-resize-mutation'
import { useDatabaseSizeQuery } from '@/data/database/database-size-query'
import { useMaxConnectionsQuery } from '@/data/database/max-connections-query'
import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query'
import { getReportAttributesV2 } from '@/data/reports/database-charts'
import { useDatabaseReport } from '@/data/reports/database-report-query'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useRefreshHandler, useReportDateRange } from '@/hooks/misc/useReportDateRange'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useIsHighAvailability, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
import { formatBytes } from '@/lib/helpers'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
import type { NextPageWithLayout } from '@/types'
const DatabaseReport: NextPageWithLayout = () => {
return (
<ReportPadding>
<DatabaseUsage />
</ReportPadding>
)
}
DatabaseReport.getLayout = (page) => (
<DefaultLayout>
<ObservabilityLayout title="Database">{page}</ObservabilityLayout>
</DefaultLayout>
)
export type UpdateDateRange = (from: string, to: string) => void
export default DatabaseReport
const REPORT_TITLE = 'Database'
const DatabaseUsage = () => {
const { db, chart, ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
const isHighAvailability = useIsHighAvailability()
const {
selectedDateRange,
updateDateRange,
datePickerValue,
datePickerHelpers,
showUpgradePrompt,
setShowUpgradePrompt,
handleDatePickerChange,
} = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES)
const state = useDatabaseSelectorStateSnapshot()
const queryClient = useQueryClient()
const [isRefreshing, setIsRefreshing] = useState(false)
const [showIncreaseDiskSizeModal, setshowIncreaseDiskSizeModal] = useState(false)
const [showDatePicker, setShowDatePicker] = useState(false)
const isReplicaSelected = state.selectedDatabaseId !== project?.ref
const report = useDatabaseReport()
const { data, params, largeObjectsSql, isPending: isLoading, refresh } = report
const { data: databaseSizeData } = useDatabaseSizeQuery({
projectRef: project?.ref,
connectionString: project?.connectionString || undefined,
})
const databaseSizeBytes = databaseSizeData ?? 0
const currentDiskSize = project?.volumeSizeGb ?? 0
const { data: diskConfig } = useDiskAttributesQuery({ projectRef: project?.ref })
const { data: maxConnections } = useMaxConnectionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
usePgbouncerConfigQuery({ projectRef: project?.ref })
// PGBouncer connections
const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref })
const computeInstance = addons?.selected_addons.find((addon) => addon.type === 'compute_instance')
const poolingOptimizations =
POOLING_OPTIMIZATIONS[
(computeInstance?.variant.identifier as keyof typeof POOLING_OPTIMIZATIONS) ??
(project?.infra_compute_size === 'nano' ? 'ci_nano' : 'ci_micro')
]
const defaultMaxClientConn = poolingOptimizations.maxClientConn ?? 200
const { can: canUpdateDiskSizeConfig } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'projects',
{
resource: {
project_id: project?.id,
},
}
)
const { getEntitlementSetValues, isLoading: isEntitlementLoading } = useCheckEntitlements(
'observability.dashboard_advanced_metrics'
)
const entitledFeatures = getEntitlementSetValues()
const isSpendCapEnabled = entitledFeatures.includes('database') && !org?.usage_billing_enabled
const showDiskIOBurstBalanceChart = useFlag('showDiskIOBurstBalanceChart')
const showMemoryCommitmentChart = useFlag('showMemoryCommitmentChart')
const REPORT_ATTRIBUTES = getReportAttributesV2(
entitledFeatures,
project!,
diskConfig,
maxConnections,
defaultMaxClientConn,
isSpendCapEnabled,
showDiskIOBurstBalanceChart,
showMemoryCommitmentChart
)
const { isPending: isUpdatingDiskSize } = useProjectDiskResizeMutation({
onSuccess: (_, variables) => {
toast.success(`Successfully updated disk size to ${variables.volumeSize} GB`)
setshowIncreaseDiskSizeModal(false)
},
})
const onRefreshReport = useRefreshHandler(
datePickerValue,
datePickerHelpers,
handleDatePickerChange,
async () => {
if (!selectedDateRange) return
setIsRefreshing(true)
refresh()
const { period_start, period_end, interval } = selectedDateRange
REPORT_ATTRIBUTES.flatMap((chart) => chart.attributes || [])
.filter((attr): attr is MultiAttribute => attr !== false)
.forEach((attr) => {
queryClient.invalidateQueries({
queryKey: analyticsKeys.infraMonitoring(ref, {
attribute: attr.attribute,
startDate: period_start.date,
endDate: period_end.date,
interval,
databaseIdentifier: state.selectedDatabaseId,
}),
})
})
if (isReplicaSelected) {
queryClient.invalidateQueries({
queryKey: analyticsKeys.infraMonitoring(ref, {
attribute: 'physical_replication_lag_physical_replication_lag_seconds',
startDate: period_start.date,
endDate: period_end.date,
interval,
databaseIdentifier: state.selectedDatabaseId,
}),
})
}
setTimeout(() => setIsRefreshing(false), 1000)
}
)
useShortcut(SHORTCUT_IDS.OBSERVABILITY_REFRESH, onRefreshReport, {
enabled: !isRefreshing,
})
useShortcut(SHORTCUT_IDS.OBSERVABILITY_TOGGLE_DATE_PICKER, () => {
setShowDatePicker((open) => !open)
})
const stateSyncedFromUrlRef = useRef(false)
useEffect(() => {
if (stateSyncedFromUrlRef.current) return
stateSyncedFromUrlRef.current = true
if (db !== undefined) {
setTimeout(() => {
// [Joshen] Adding a timeout here to support navigation from settings to reports
// Both are rendering different instances of ProjectLayout which is where the
// DatabaseSelectorContextProvider lies in (unless we reckon shifting the provider up one more level is better)
state.setSelectedDatabaseId(db)
}, 100)
}
if (chart !== undefined) {
setTimeout(() => {
const el = document.getElementById(chart)
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 200)
}
}, [db, chart, state])
return (
<>
<ReportHeader showDatabaseSelector title={REPORT_TITLE} />
<ReportStickyNav
content={
<div className="ml-auto flex items-center gap-2 flex-wrap">
<DocsButton href={OBSERVABILITY_DOCS_HREFS.database} topic={REPORT_TITLE} />
<ShortcutTooltip
shortcutId={SHORTCUT_IDS.OBSERVABILITY_REFRESH}
label="Refresh report"
side="bottom"
>
<Button
aria-label="Refresh report"
variant="default"
disabled={isRefreshing}
icon={<RefreshCw className={isRefreshing ? 'animate-spin' : ''} />}
className="w-7"
onClick={onRefreshReport}
/>
</ShortcutTooltip>
<ReportSettings chartId="database-charts" />
<div className="flex items-center gap-3">
<LogsDatePicker
onSubmit={handleDatePickerChange}
value={datePickerValue}
helpers={datePickerHelpers}
open={showDatePicker}
onOpenChange={setShowDatePicker}
shortcutId={SHORTCUT_IDS.OBSERVABILITY_TOGGLE_DATE_PICKER}
/>
<UpgradePrompt
show={showUpgradePrompt}
setShowUpgradePrompt={setShowUpgradePrompt}
title="Report date range"
description="Report data can be stored for a maximum of 3 months depending on the plan that your project is on."
source="databaseReportDateRange"
/>
{selectedDateRange && (
<div className="flex items-center gap-x-2 text-xs">
<p className="text-foreground-light">
{dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')}
</p>
<p className="text-foreground-light">
<ArrowRight size={12} />
</p>
<p className="text-foreground-light">
{dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')}
</p>
</div>
)}
</div>
</div>
}
>
{selectedDateRange &&
REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart) => {
const chartAvailable =
!chart.entitlement ||
isEntitlementLoading ||
entitledFeatures.includes(chart.entitlement)
return chartAvailable ? (
<LazyComposedChartHandler
key={chart.id}
{...chart}
attributes={chart.attributes as MultiAttribute[]}
interval={selectedDateRange.interval}
startDate={selectedDateRange?.period_start?.date}
endDate={selectedDateRange?.period_end?.date}
updateDateRange={updateDateRange}
defaultChartStyle={chart.defaultChartStyle as 'line' | 'bar' | 'stackedAreaLine'}
syncId="database-charts"
showMaxValue={
chart.id === 'client-connections' ||
chart.id === 'client-connections-basic' ||
chart.id === 'pgbouncer-connections'
? true
: chart.showMaxValue
}
/>
) : (
<ReportChartUpsell
key={chart.id}
report={{ label: chart.label, requiredPlan: chart.requiredPlan }}
orgSlug={org?.slug ?? ''}
/>
)
})}
{selectedDateRange && isReplicaSelected && (
<LazyComposedChartHandler
id="replication-lag"
label="Replication lag"
format="s"
valuePrecision={2}
showTooltip
YAxisProps={{
width: 50,
tickFormatter: (value: number) => `${value}s`,
}}
attributes={[
{
attribute: 'physical_replication_lag_physical_replication_lag_seconds',
provider: 'infra-monitoring',
label: 'Replication lag',
tooltip:
'Seconds the read replica is behind its primary. Sustained or growing lag may indicate the replica cannot keep up with write throughput',
},
]}
interval={selectedDateRange.interval}
startDate={selectedDateRange?.period_start?.date}
endDate={selectedDateRange?.period_end?.date}
updateDateRange={updateDateRange}
defaultChartStyle="line"
syncId="database-charts"
/>
)}
</ReportStickyNav>
<section id="database-size-report">
<ReportWidget
isLoading={isLoading}
params={params.largeObjects}
title="Database Size"
data={data.largeObjects || []}
queryType={'db'}
resolvedSql={largeObjectsSql}
renderer={(props) => {
return (
<div>
<div className="flex flex-wrap items-center gap-8 mt-5">
<div className="flex flex-col gap-1">
<p className="text-sm text-foreground-light">Space used</p>
<span className="text-lg font-semibold text-foreground">
{formatBytes(databaseSizeBytes, 2, 'GB')}
</span>
</div>
<div className="flex flex-col gap-1">
<p className="text-sm text-foreground-light">Provisioned disk size</p>
<span className="text-lg font-semibold text-foreground">
{currentDiskSize} GB
</span>
</div>
<div className="ml-auto">
{/*
[Joshen] TODO: Check if this check is still relevant
The DiskSizeConfigurationModal is old and might be obsolete
*/}
{project?.cloud_provider === 'AWS' && !isHighAvailability ? (
<Button asChild variant="default">
<Link href={getInfrastructurePath(ref)}>Increase disk size</Link>
</Button>
) : (
<ButtonTooltip
variant="default"
disabled={!canUpdateDiskSizeConfig || isHighAvailability}
onClick={() => setshowIncreaseDiskSizeModal(true)}
tooltip={{
content: {
side: 'bottom',
text: !canUpdateDiskSizeConfig
? 'You need additional permissions to increase the disk size'
: isHighAvailability
? 'Disk size management is unavailable for High Availability projects'
: undefined,
},
}}
>
Increase disk size
</ButtonTooltip>
)}
</div>
</div>
<p className="mt-8 text-sm font-medium text-foreground-light">Large Objects</p>
{!props.isLoading && props.data.length === 0 && (
<span className="text-sm text-foreground-light mt-2 block">
No large objects found
</span>
)}
{!props.isLoading && props.data.length > 0 && (
<Table
className="space-y-3 mt-4"
head={[
<Table.th key="object" className="py-2">
Object
</Table.th>,
<Table.th key="size" className="py-2">
Size
</Table.th>,
]}
body={props.data?.map((object) => {
const percentage = (
((object.table_size as number) / databaseSizeBytes) *
100
).toFixed(2)
return (
<Table.tr key={`${object.schema_name}.${object.relname}`}>
<Table.td>
{object.schema_name}.{object.relname}
</Table.td>
<Table.td>
{formatBytes(object.table_size)} ({percentage}%)
</Table.td>
</Table.tr>
)
})}
/>
)}
</div>
)
}}
append={renderDatabaseSizeAdditionalInfo}
/>
<DiskSizeConfigurationModal
visible={showIncreaseDiskSizeModal}
loading={isUpdatingDiskSize}
hideModal={setshowIncreaseDiskSizeModal}
/>
</section>
<div className="py-8">
<ObservabilityLink />
</div>
</>
)
}
const renderDatabaseSizeAdditionalInfo = () => {
return (
<div className="px-6 pb-6">
<Alert variant="default" className="mt-4">
<AlertDescription>
<div className="space-y-2">
<p>
New Supabase projects have a database size of ~40-60mb. This space includes
pre-installed extensions, schemas, and default Postgres data. Additional database size
is used when installing extensions, even if those extensions are inactive.
</p>
<Button asChild variant="default" icon={<ExternalLink />}>
<Link
href={`${DOCS_URL}/guides/platform/database-size#disk-space-usage`}
target="_blank"
rel="noreferrer"
>
Read about database size
</Link>
</Button>
</div>
</AlertDescription>
</Alert>
</div>
)
}