Chore/support checking replication lag in database reports (#26290)
* Init replication lag chart in database reports * Navigate to replication lag chart via query params * Add view replication lag CTA in map view * Add replication lag metric to instance node view * Prettier
This commit is contained in:
@@ -1,12 +1,32 @@
|
||||
import { Button, IconRefreshCw } from 'ui'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
interface Props {
|
||||
import { useParams } from 'common'
|
||||
import DatabaseSelector from 'components/ui/DatabaseSelector'
|
||||
|
||||
interface ReportHeaderProps {
|
||||
title: string
|
||||
showDatabaseSelector?: boolean
|
||||
}
|
||||
|
||||
const ReportHeader: React.FC<Props> = ({ title }) => (
|
||||
<div className="flex flex-row justify-between gap-4 items-center">
|
||||
<h1 className="text-2xl text-foreground">{title}</h1>
|
||||
</div>
|
||||
)
|
||||
const ReportHeader = ({ title, showDatabaseSelector }: ReportHeaderProps) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { db, chart, ...params } = router.query
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between gap-4 items-center">
|
||||
<h1 className="text-2xl text-foreground">{title}</h1>
|
||||
{showDatabaseSelector && (
|
||||
<DatabaseSelector
|
||||
onSelectId={(db) => {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: db !== ref ? { ...params, db } : params,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ReportHeader
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
/**
|
||||
* Standardized padding and width layout for non-custom reports
|
||||
*/
|
||||
const ReportPadding: React.FC<React.PropsWithChildren> = ({ children }) => (
|
||||
const ReportPadding = ({ children }: PropsWithChildren<{}>) => (
|
||||
<div className="flex flex-col gap-4 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 xl:px-24 2xl:px-32">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -67,9 +67,7 @@ export function ChartConfig({ results = { rows: [] }, config, onConfigChange }:
|
||||
<div className="p-2">
|
||||
<NoDataPlaceholder
|
||||
size="normal"
|
||||
message="
|
||||
Execute a query and configure the chart options.
|
||||
"
|
||||
description="Execute a query and configure the chart options."
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -169,7 +169,7 @@ export const DatabaseConnectionString = ({ appearance }: DatabaseConnectionStrin
|
||||
<div id="connection-string" className="w-full">
|
||||
<Panel
|
||||
className={cn(
|
||||
'!m-0 [&>div:nth-child(1)]:!border-0 [&>div:nth-child(1)>div]:!p-0',
|
||||
'!m-0 [&>div:nth-child(1)]:!border-0 [&>div:nth-child(1)]:!p-0',
|
||||
appearance === 'minimal' && 'border-0 shadow-none'
|
||||
)}
|
||||
bodyClassName={cn(appearance === 'minimal' && 'bg-transparent')}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useParams } from 'common'
|
||||
import { useReplicationLagQuery } from 'data/read-replicas/replica-lag-query'
|
||||
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import type { EdgeProps } from 'reactflow'
|
||||
import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from 'reactflow'
|
||||
import { TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui'
|
||||
|
||||
export const SmoothstepEdge = ({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
style = {},
|
||||
markerEnd,
|
||||
data,
|
||||
}: EdgeProps) => {
|
||||
const { ref } = useParams()
|
||||
// [Joshen] Only applicable for replicas
|
||||
const { identifier, connectionString } = data || {}
|
||||
const formattedId = formatDatabaseID(identifier ?? '')
|
||||
|
||||
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
})
|
||||
|
||||
const {
|
||||
data: lagDuration,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useReplicationLagQuery({
|
||||
id: identifier,
|
||||
projectRef: ref,
|
||||
connectionString,
|
||||
})
|
||||
const lagValue = Number(lagDuration?.toFixed(2) ?? 0).toLocaleString()
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
|
||||
{data !== undefined && !isError && (
|
||||
<EdgeLabelRenderer>
|
||||
<Tooltip_Shadcn_>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<div
|
||||
className="bg-surface-100 px-1.5 py-0.5 rounded absolute nodrag nopan"
|
||||
style={{
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
pointerEvents: 'all',
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={12} className="animate-spin" />
|
||||
) : (
|
||||
<p className="font-mono text-xs">{lagValue}s</p>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="bottom" align="center">
|
||||
{isLoading
|
||||
? `Checking replication lag for replica ID: ${formattedId}`
|
||||
: `Replication lag (seconds) for replica ID: ${formattedId}`}
|
||||
</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { addRegionNodes, generateNodes, getDagreGraphLayout } from './InstanceCo
|
||||
import { LoadBalancerNode, PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode'
|
||||
import MapView from './MapView'
|
||||
import { RestartReplicaConfirmationModal } from './RestartReplicaConfirmationModal'
|
||||
import { SmoothstepEdge } from './Edge'
|
||||
|
||||
// [Joshen] Just FYI, UI assumes single provider for primary + replicas
|
||||
// [Joshen] Idea to visualize grouping based on region: https://reactflow.dev/examples/layout/sub-flows
|
||||
@@ -142,6 +143,10 @@ const InstanceConfigurationUI = () => {
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
className: '!cursor-default',
|
||||
data: {
|
||||
identifier: database.identifier,
|
||||
connectionString: database.connectionString,
|
||||
},
|
||||
}
|
||||
}),
|
||||
]
|
||||
@@ -159,6 +164,10 @@ const InstanceConfigurationUI = () => {
|
||||
[]
|
||||
)
|
||||
|
||||
const edgeTypes = {
|
||||
smoothstep: SmoothstepEdge,
|
||||
}
|
||||
|
||||
const setReactFlow = async () => {
|
||||
const graph = getDagreGraphLayout(nodes, edges)
|
||||
const { nodes: updatedNodes } = addRegionNodes(graph.nodes, graph.edges)
|
||||
@@ -250,6 +259,7 @@ const InstanceConfigurationUI = () => {
|
||||
defaultNodes={[]}
|
||||
defaultEdges={[]}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background color={backgroundPatternColor} />
|
||||
|
||||
@@ -269,6 +269,14 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
|
||||
View connection string
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={status !== PROJECT_STATUS.ACTIVE_HEALTHY}
|
||||
className="gap-x-2"
|
||||
>
|
||||
<Link href={`/project/${ref}/reports/database?db=${id}&chart=replication-lag`}>
|
||||
View replication lag
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectRestartReplica()}>
|
||||
Restart replica
|
||||
|
||||
@@ -287,7 +287,16 @@ const MapView = ({
|
||||
View connection string
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="gap-x-2">
|
||||
<Link
|
||||
href={`/project/${ref}/reports/database?db=${database.identifier}&chart=replication-lag`}
|
||||
>
|
||||
View replication lag
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
className="gap-x-2"
|
||||
onClick={() => onSelectRestartReplica(database)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isUndefined } from 'lodash'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren, useState } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import { Button, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import AreaChart from 'components/ui/Charts/AreaChart'
|
||||
import BarChart from 'components/ui/Charts/BarChart'
|
||||
import { AnalyticsInterval } from 'data/analytics/constants'
|
||||
import {
|
||||
InfraMonitoringAttribute,
|
||||
@@ -15,9 +15,9 @@ import { Activity, BarChartIcon, Loader2 } from 'lucide-react'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { WarningIcon } from 'ui-patterns/Icons/StatusIcons'
|
||||
import type { ChartData } from './ChartHandler.types'
|
||||
import { BarChart } from './ChartRenderer'
|
||||
|
||||
interface ChartHandlerProps {
|
||||
id?: string
|
||||
label: string
|
||||
attribute: string
|
||||
provider: 'infra-monitoring' | 'daily-stats'
|
||||
@@ -139,7 +139,7 @@ const ChartHandler = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (isUndefined(chartData)) {
|
||||
if (chartData === undefined) {
|
||||
return (
|
||||
<div className="flex h-52 w-full flex-col items-center justify-center gap-y-2">
|
||||
<WarningIcon />
|
||||
@@ -152,30 +152,31 @@ const ChartHandler = ({
|
||||
<div className="h-full w-full">
|
||||
<div className="absolute right-6 z-50 flex justify-between">
|
||||
{!hideChartType && (
|
||||
<div>
|
||||
<div className="flex w-full space-x-3">
|
||||
<Tooltip_Shadcn_>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1.5"
|
||||
icon={chartStyle === 'bar' ? <Activity /> : <BarChartIcon />}
|
||||
onClick={() => setChartStyle(chartStyle === 'bar' ? 'line' : 'bar')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="left" align="center">
|
||||
View as {chartStyle === 'bar' ? 'line chart' : 'bar chart'}
|
||||
</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{chartStyle === 'bar' ? (
|
||||
<BarChart
|
||||
data={chartData?.data ?? []}
|
||||
attribute={attribute}
|
||||
yAxisLimit={chartData?.yAxisLimit}
|
||||
data={(chartData?.data ?? []) as any}
|
||||
format={format || chartData?.format}
|
||||
xAxisKey={'period_start'}
|
||||
yAxisKey={attribute}
|
||||
highlightedValue={_highlightedValue}
|
||||
label={label}
|
||||
title={label}
|
||||
customDateFormat={customDateFormat}
|
||||
onBarClick={onBarClick}
|
||||
/>
|
||||
) : (
|
||||
<AreaChart
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Bar,
|
||||
BarChart as RechartBarChart,
|
||||
Cell,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
import { Loading } from 'ui'
|
||||
|
||||
import { CHART_COLORS } from 'components/ui/Charts/Charts.constants'
|
||||
import EmptyState from 'components/ui/Charts/EmptyState'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
|
||||
function dataCheck(data: any, attribute: any) {
|
||||
const hasData = data && data.find((record: any) => record[attribute])
|
||||
return hasData ? true : false
|
||||
}
|
||||
|
||||
const CustomTooltip = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
const DATE_FORMAT__WITH_TIME = 'MMM D, YYYY, hh:mma'
|
||||
const DATE_FORMAT__DATE_ONLY = 'MMM D, YYYY'
|
||||
|
||||
const Header = ({
|
||||
attribute,
|
||||
focus,
|
||||
format,
|
||||
highlightedValue,
|
||||
data,
|
||||
customDateFormat,
|
||||
label,
|
||||
minimalHeader = false,
|
||||
displayDateInUtc = false,
|
||||
}: any) => {
|
||||
let FOCUS_FORMAT = customDateFormat
|
||||
? customDateFormat
|
||||
: format == '%'
|
||||
? DATE_FORMAT__WITH_TIME
|
||||
: DATE_FORMAT__DATE_ONLY
|
||||
|
||||
let title = ''
|
||||
|
||||
const isByteAttribute =
|
||||
format === 'bytes' ||
|
||||
attribute.includes('ingress') ||
|
||||
attribute.includes('egress') ||
|
||||
attribute.includes('bytes')
|
||||
|
||||
if (focus) {
|
||||
if (!data) {
|
||||
title = ''
|
||||
} else if (format === '%') {
|
||||
title = Number(data[focus]?.[attribute]).toFixed(2)
|
||||
} else {
|
||||
if (isByteAttribute) {
|
||||
title = formatBytes(data[focus]?.[attribute])
|
||||
} else {
|
||||
title = data[focus]?.[attribute]?.toLocaleString()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (format === '%' && highlightedValue) {
|
||||
title = highlightedValue.toFixed(2)
|
||||
} else {
|
||||
if (isByteAttribute) {
|
||||
title = formatBytes(highlightedValue)
|
||||
} else {
|
||||
title = highlightedValue?.toLocaleString()
|
||||
}
|
||||
}
|
||||
}
|
||||
const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value))
|
||||
|
||||
const chartTitle = (
|
||||
<h3 className={'text-foreground-lighter ' + (minimalHeader ? 'text-xs' : 'text-sm')}>
|
||||
{label ?? attribute}
|
||||
</h3>
|
||||
)
|
||||
const highlighted = (
|
||||
<h5
|
||||
className={
|
||||
'text-xl font-normal text-foreground ' + (minimalHeader ? 'text-base' : 'text-2xl')
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{!isByteAttribute && <span className="text-lg">{format}</span>}
|
||||
</h5>
|
||||
)
|
||||
const date = (
|
||||
<h5 className="text-xs text-foreground-lighter">
|
||||
{focus ? (
|
||||
data && data[focus] && day(data[focus].period_start).format(FOCUS_FORMAT)
|
||||
) : (
|
||||
<span className="opacity-0">x</span>
|
||||
)}
|
||||
</h5>
|
||||
)
|
||||
|
||||
if (minimalHeader) {
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-x-4" style={{ minHeight: '1.8rem' }}>
|
||||
{chartTitle}
|
||||
<div className="flex flex-row items-baseline gap-x-2">
|
||||
{highlighted}
|
||||
{date}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{chartTitle}
|
||||
{highlighted}
|
||||
{date}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated please use studio/components/ui/Charts/BarChart.tsx instead
|
||||
*/
|
||||
export function BarChart({
|
||||
data,
|
||||
attribute,
|
||||
yAxisLimit,
|
||||
format,
|
||||
highlightedValue,
|
||||
customDateFormat,
|
||||
displayDateInUtc = false,
|
||||
label,
|
||||
onBarClick,
|
||||
minimalHeader,
|
||||
chartSize = 'normal',
|
||||
className = '',
|
||||
noDataTitle,
|
||||
noDataMessage,
|
||||
}: any) {
|
||||
const hasData = data ? dataCheck(data, attribute) : true
|
||||
|
||||
const [focusBar, setFocusBar] = useState<any>(null)
|
||||
const [mouseLeave, setMouseLeave] = useState<any>(true)
|
||||
|
||||
const onMouseMove = (state: any) => {
|
||||
if (state?.activeTooltipIndex) {
|
||||
setFocusBar(state.activeTooltipIndex)
|
||||
setMouseLeave(false)
|
||||
} else {
|
||||
setFocusBar(null)
|
||||
setMouseLeave(true)
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseLeave = () => {
|
||||
setFocusBar(false)
|
||||
setMouseLeave(true)
|
||||
}
|
||||
|
||||
const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value))
|
||||
|
||||
// For future reference: https://github.com/supabase/supabase/pull/5311#discussion_r800852828
|
||||
const chartHeight = {
|
||||
tiny: 76,
|
||||
small: 96,
|
||||
normal: 160,
|
||||
}[chartSize as string] as number
|
||||
|
||||
return (
|
||||
<Loading active={!data}>
|
||||
<div className={className}>
|
||||
<Header
|
||||
minimalHeader={minimalHeader}
|
||||
attribute={attribute}
|
||||
focus={focusBar}
|
||||
highlightedValue={highlightedValue}
|
||||
data={data}
|
||||
label={label}
|
||||
format={format}
|
||||
customDateFormat={customDateFormat}
|
||||
displayDateInUtc={displayDateInUtc}
|
||||
/>
|
||||
<div style={{ width: '100%', height: `${chartHeight}px` }}>
|
||||
{hasData ? (
|
||||
<>
|
||||
<ResponsiveContainer width="100%" height={chartHeight}>
|
||||
<RechartBarChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
className="cursor-pointer overflow-visible"
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={(tooltipData: any) => {
|
||||
// receives tooltip data https://github.com/recharts/recharts/blob/2a3405ff64a0c050d2cf94c36f0beef738d9e9c2/src/chart/generateCategoricalChart.tsx
|
||||
if (onBarClick) onBarClick(tooltipData)
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="period_start"
|
||||
//interval={size === 'small' ? 5 : 1}
|
||||
interval={data ? data.length - 2 : 0}
|
||||
angle={0}
|
||||
// stroke="#4B5563"
|
||||
tick={false}
|
||||
axisLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
tickLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
{/* <YAxis dataKey={attribute} /> */}
|
||||
{/* <YAxis type="number" domain={[(0, 100)]} /> */}
|
||||
{yAxisLimit && <YAxis type="number" domain={[0, yAxisLimit]} hide />}
|
||||
<Bar
|
||||
dataKey={attribute}
|
||||
fill={CHART_COLORS.GREEN_1}
|
||||
// barSize={2}
|
||||
animationDuration={300}
|
||||
// max bar size required for LogEventChart, prevents bars from expanding to max width.
|
||||
maxBarSize={48}
|
||||
>
|
||||
{data?.map((entry: any, index: any) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
className={`transition-all duration-300 ${
|
||||
onBarClick ? 'cursor-pointer' : ''
|
||||
}`}
|
||||
fill={
|
||||
focusBar === index || mouseLeave
|
||||
? CHART_COLORS.GREEN_1
|
||||
: CHART_COLORS.GREEN_2
|
||||
}
|
||||
enableBackground={12}
|
||||
// for this, we make the hovered colour #2B5CE7, else its opacity decreases to 20%
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</RechartBarChart>
|
||||
</ResponsiveContainer>
|
||||
{data && (
|
||||
<div className="-mt-5 flex items-center justify-between text-xs text-foreground-lighter">
|
||||
<span>
|
||||
{day(data[0].period_start).format(
|
||||
customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{day(data[data?.length - 1]?.period_start).format(
|
||||
customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<EmptyState title={noDataTitle} message={noDataMessage} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Loading>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { CHART_COLORS, DateTimeFormats } from 'components/ui/Charts/Charts.constants'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { useState } from 'react'
|
||||
import { Area, AreaChart as RechartAreaChart, Tooltip, XAxis } from 'recharts'
|
||||
|
||||
import { CHART_COLORS, DateTimeFormats } from 'components/ui/Charts/Charts.constants'
|
||||
import ChartHeader from './ChartHeader'
|
||||
import type { CommonChartProps, Datum } from './Charts.types'
|
||||
import { numberFormatter, useChartSize } from './Charts.utils'
|
||||
import ChartNoData from './NoDataPlaceholder'
|
||||
dayjs.extend(utc)
|
||||
import NoDataPlaceholder from './NoDataPlaceholder'
|
||||
|
||||
export interface AreaChartProps<D = Datum> extends CommonChartProps<D> {
|
||||
yAxisKey: string
|
||||
@@ -35,8 +34,6 @@ const AreaChart = ({
|
||||
const { Container } = useChartSize(size)
|
||||
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
|
||||
|
||||
if (data.length === 0) return <ChartNoData size={size} className={className} />
|
||||
|
||||
const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value))
|
||||
const resolvedHighlightedLabel =
|
||||
(focusDataIndex !== null &&
|
||||
@@ -48,6 +45,18 @@ const AreaChart = ({
|
||||
const resolvedHighlightedValue =
|
||||
focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<NoDataPlaceholder
|
||||
description="It may take up to 24 hours for data to show"
|
||||
size={size}
|
||||
className={className}
|
||||
attribute={title}
|
||||
format={format}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={['flex flex-col gap-3', className].join(' ')}>
|
||||
<ChartHeader
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { CHART_COLORS, DateTimeFormats } from 'components/ui/Charts/Charts.constants'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { useState } from 'react'
|
||||
import { Bar, BarChart as RechartBarChart, Cell, Tooltip, XAxis, Legend } from 'recharts'
|
||||
import { Bar, Cell, Legend, BarChart as RechartBarChart, Tooltip, XAxis } from 'recharts'
|
||||
|
||||
import { CHART_COLORS, DateTimeFormats } from 'components/ui/Charts/Charts.constants'
|
||||
import type { CategoricalChartState } from 'recharts/types/chart/generateCategoricalChart'
|
||||
import ChartHeader from './ChartHeader'
|
||||
import type { CommonChartProps, Datum } from './Charts.types'
|
||||
import { numberFormatter, useChartSize } from './Charts.utils'
|
||||
import ChartNoData from './NoDataPlaceholder'
|
||||
dayjs.extend(utc)
|
||||
import NoDataPlaceholder from './NoDataPlaceholder'
|
||||
|
||||
export interface BarChartProps<D = Datum> extends CommonChartProps<D> {
|
||||
yAxisKey: string
|
||||
@@ -43,9 +42,6 @@ const BarChart = ({
|
||||
const { Container } = useChartSize(size)
|
||||
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
|
||||
|
||||
if (data.length === 0)
|
||||
return <ChartNoData message={emptyStateMessage} size={size} className={className} />
|
||||
|
||||
const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value))
|
||||
|
||||
function getHeaderLabel() {
|
||||
@@ -67,6 +63,19 @@ const BarChart = ({
|
||||
const resolvedHighlightedValue =
|
||||
focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<NoDataPlaceholder
|
||||
message={emptyStateMessage}
|
||||
description="It may take up to 24 hours for data to show"
|
||||
size={size}
|
||||
className={className}
|
||||
attribute={title}
|
||||
format={format}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={['flex flex-col gap-3', className].join(' ')}>
|
||||
<ChartHeader
|
||||
|
||||
@@ -7,7 +7,8 @@ export interface ChartHeaderProps {
|
||||
highlightedLabel?: number | string | null
|
||||
highlightedValue?: number | string | null
|
||||
}
|
||||
const ChartHeader: React.FC<ChartHeaderProps> = ({
|
||||
|
||||
const ChartHeader = ({
|
||||
format,
|
||||
highlightedValue,
|
||||
highlightedLabel,
|
||||
@@ -25,6 +26,7 @@ const ChartHeader: React.FC<ChartHeaderProps> = ({
|
||||
className={`text-foreground text-xl font-normal ${minimalHeader ? 'text-base' : 'text-2xl'}`}
|
||||
>
|
||||
{highlightedValue !== undefined && String(highlightedValue)}
|
||||
{format === 'seconds' ? ' ' : ''}
|
||||
<span className="text-lg">
|
||||
{typeof format === 'function' ? format(highlightedValue) : format}
|
||||
</span>
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
import { BarChart2 } from 'lucide-react'
|
||||
import { useChartSize } from './Charts.utils'
|
||||
import ChartHeader from './ChartHeader'
|
||||
|
||||
interface Props {
|
||||
interface NoDataPlaceholderProps {
|
||||
title?: string
|
||||
attribute?: string
|
||||
format?: string | ((value: unknown) => string)
|
||||
message?: string
|
||||
description?: string
|
||||
className?: string
|
||||
size: Parameters<typeof useChartSize>[0]
|
||||
}
|
||||
const NoDataPlaceholder: React.FC<Props> = ({
|
||||
title = 'No data to show',
|
||||
message,
|
||||
const NoDataPlaceholder = ({
|
||||
attribute,
|
||||
message = 'No data to show',
|
||||
description,
|
||||
format,
|
||||
className = '',
|
||||
size,
|
||||
}) => {
|
||||
}: NoDataPlaceholderProps) => {
|
||||
const { minHeight } = useChartSize(size)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'border-control flex flex-grow w-full flex-col items-center justify-center space-y-2 border border-dashed text-center ' +
|
||||
className
|
||||
}
|
||||
// extra 20 px for the x ticks
|
||||
style={{ minHeight: minHeight + 20 }}
|
||||
>
|
||||
<BarChart2 size={20} className="text-border-stronger" />
|
||||
<div>
|
||||
<p className="text-foreground-light text-xs">{title}</p>
|
||||
{message && <p className="text-foreground-lighter text-xs">{message}</p>}
|
||||
<div>
|
||||
{attribute !== undefined && (
|
||||
<ChartHeader title={attribute} format={format} highlightedValue={0} />
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
'border-control flex flex-grow w-full flex-col items-center justify-center space-y-2 border border-dashed text-center ' +
|
||||
className
|
||||
}
|
||||
// extra 20 px for the x ticks
|
||||
style={{ minHeight: minHeight + 20 }}
|
||||
>
|
||||
<BarChart2 size={20} className="text-border-stronger" />
|
||||
<div>
|
||||
<p className="text-foreground-light text-xs">{message}</p>
|
||||
{description && <p className="text-foreground-lighter text-xs">{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useParams } from 'common'
|
||||
import { noop } from 'lodash'
|
||||
import { Check } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
@@ -23,12 +24,11 @@ import {
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { REPLICA_STATUS } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
|
||||
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
|
||||
import { formatDatabaseID, formatDatabaseRegion } from 'data/read-replicas/replicas.utils'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { Check } from 'lucide-react'
|
||||
import { REPLICA_STATUS } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
|
||||
interface DatabaseSelectorProps {
|
||||
variant?: 'regular' | 'connected-on-right' | 'connected-on-left' | 'connected-on-both'
|
||||
|
||||
44
apps/studio/data/read-replicas/replica-lag-query.ts
Normal file
44
apps/studio/data/read-replicas/replica-lag-query.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { UseQueryOptions } from '@tanstack/react-query'
|
||||
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
|
||||
|
||||
export const replicationLagQuery = () => {
|
||||
const sql = /* SQL */ `
|
||||
select
|
||||
case
|
||||
when (select count(*) from pg_stat_wal_receiver) = 1 and pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()
|
||||
then 0
|
||||
else coalesce(extract(epoch from now() - pg_last_xact_replay_timestamp()),0)
|
||||
end as physical_replica_lag_second
|
||||
`
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export type ReplicationLagVariables = {
|
||||
id: string
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
}
|
||||
|
||||
export type ReplicationLagData = number
|
||||
export type ReplicationLagError = unknown
|
||||
|
||||
export const useReplicationLagQuery = <TData extends ReplicationLagData = ReplicationLagData>(
|
||||
{ projectRef, connectionString, id }: ReplicationLagVariables,
|
||||
{ enabled, ...options }: UseQueryOptions<ExecuteSqlData, ReplicationLagError, TData> = {}
|
||||
) =>
|
||||
useExecuteSqlQuery(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: replicationLagQuery(),
|
||||
queryKey: ['replica-lag', id],
|
||||
},
|
||||
{
|
||||
select(data) {
|
||||
return Number((data.result[0] ?? null)?.physical_replica_lag_second ?? 0) as TData
|
||||
},
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
31
apps/studio/data/reports/database-report-query.ts
Normal file
31
apps/studio/data/reports/database-report-query.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useParams } from 'common'
|
||||
import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants'
|
||||
import { queriesFactory } from 'components/interfaces/Reports/Reports.utils'
|
||||
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
|
||||
|
||||
export const useDatabaseReport = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const queryHooks = queriesFactory<keyof typeof PRESET_CONFIG.database.queries>(
|
||||
PRESET_CONFIG.database.queries,
|
||||
projectRef ?? 'default'
|
||||
)
|
||||
const largeObjects = queryHooks.largeObjects() as DbQueryHook
|
||||
const activeHooks = [largeObjects]
|
||||
|
||||
const isLoading = activeHooks.some((hook) => hook.isLoading)
|
||||
|
||||
return {
|
||||
data: {
|
||||
largeObjects: largeObjects.data,
|
||||
},
|
||||
errors: {
|
||||
largeObjects: largeObjects.error,
|
||||
},
|
||||
params: {
|
||||
largeObjects: largeObjects.params,
|
||||
},
|
||||
largeObjectsSql: largeObjects.resolvedSql,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,31 @@
|
||||
import { useParams } from 'common/hooks'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertDescription_Shadcn_, Alert_Shadcn_, Button, IconExternalLink } from 'ui'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
|
||||
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
|
||||
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
||||
import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants'
|
||||
import { queriesFactory } from 'components/interfaces/Reports/Reports.utils'
|
||||
import { ReportsLayout } from 'components/layouts'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
|
||||
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import DatabaseSelector from 'components/ui/DatabaseSelector'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useDatabaseSizeQuery } from 'data/database/database-size-query'
|
||||
import type { DbQueryHook } from 'hooks/analytics/useDbQuery'
|
||||
import { useDatabaseReport } from 'data/reports/database-report-query'
|
||||
import { TIME_PERIODS_INFRA } from 'lib/constants/metrics'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const DatabaseReport: NextPageWithLayout = () => {
|
||||
return (
|
||||
<div className="1xl:px-28 mx-auto flex flex-col gap-4 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32">
|
||||
<div className="content h-full w-full overflow-y-auto">
|
||||
<div className="w-full">
|
||||
<DatabaseUsage />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReportPadding>
|
||||
<DatabaseUsage />
|
||||
</ReportPadding>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,10 +34,13 @@ DatabaseReport.getLayout = (page) => <ReportsLayout title="Database">{page}</Rep
|
||||
export default DatabaseReport
|
||||
|
||||
const DatabaseUsage = () => {
|
||||
const { db, chart } = useParams()
|
||||
const { project } = useProjectContext()
|
||||
const [dateRange, setDateRange] = useState<any>(undefined)
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
|
||||
const showReadReplicasUI = project?.is_read_replicas_enabled
|
||||
const isReadReplicasEnabled = project?.is_read_replicas_enabled
|
||||
const isReplicaSelected = state.selectedDatabaseId !== project?.ref
|
||||
|
||||
const report = useDatabaseReport()
|
||||
const { data } = useDatabaseSizeQuery({
|
||||
@@ -50,205 +49,203 @@ const DatabaseUsage = () => {
|
||||
})
|
||||
const databaseSizeBytes = data?.result[0].db_size ?? 0
|
||||
|
||||
// [Joshen] Empty dependency array as we only want this running once
|
||||
useEffect(() => {
|
||||
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])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<section>
|
||||
<Panel
|
||||
title={
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<h2>Database health</h2>
|
||||
{showReadReplicasUI && <DatabaseSelector />}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content>
|
||||
<div className="mb-4 flex items-center space-x-3">
|
||||
<DateRangePicker
|
||||
loading={false}
|
||||
value={'7d'}
|
||||
options={TIME_PERIODS_INFRA}
|
||||
currentBillingPeriodStart={undefined}
|
||||
onChange={setDateRange}
|
||||
<ReportHeader title="Database" showDatabaseSelector={isReadReplicasEnabled} />
|
||||
<section>
|
||||
<Panel title={<h2>Database health</h2>}>
|
||||
<Panel.Content>
|
||||
<div className="mb-4 flex items-center space-x-3">
|
||||
<DateRangePicker
|
||||
loading={false}
|
||||
value={'7d'}
|
||||
options={TIME_PERIODS_INFRA}
|
||||
currentBillingPeriodStart={undefined}
|
||||
onChange={setDateRange}
|
||||
/>
|
||||
{dateRange && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-foreground-light">
|
||||
{dayjs(dateRange.period_start.date).format('MMMM D, hh:mma')}
|
||||
</p>
|
||||
<p className="text-foreground-light">
|
||||
<ArrowRight size={12} />
|
||||
</p>
|
||||
<p className="text-foreground-light">
|
||||
{dayjs(dateRange.period_end.date).format('MMMM D, hh:mma')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{dateRange && (
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute={'ram_usage'}
|
||||
label={'Memory usage'}
|
||||
interval={dateRange.interval}
|
||||
provider={'infra-monitoring'}
|
||||
/>
|
||||
{dateRange && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-foreground-light">
|
||||
{dayjs(dateRange.period_start.date).format('MMMM D, hh:mma')}
|
||||
</p>
|
||||
<p className="text-foreground-light">
|
||||
<ArrowRight size={12} />
|
||||
</p>
|
||||
<p className="text-foreground-light">
|
||||
{dayjs(dateRange.period_end.date).format('MMMM D, hh:mma')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{dateRange && (
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute={'ram_usage'}
|
||||
label={'Memory usage'}
|
||||
interval={dateRange.interval}
|
||||
provider={'infra-monitoring'}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
|
||||
{dateRange && (
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute={'swap_usage'}
|
||||
label={'Swap usage'}
|
||||
interval={dateRange.interval}
|
||||
provider={'infra-monitoring'}
|
||||
/>
|
||||
)}
|
||||
{dateRange && (
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute={'swap_usage'}
|
||||
label={'Swap usage'}
|
||||
interval={dateRange.interval}
|
||||
provider={'infra-monitoring'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dateRange && (
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute={'avg_cpu_usage'}
|
||||
label={'Average CPU usage'}
|
||||
interval={dateRange.interval}
|
||||
provider={'infra-monitoring'}
|
||||
/>
|
||||
)}
|
||||
{dateRange && (
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute={'avg_cpu_usage'}
|
||||
label={'Average CPU usage'}
|
||||
interval={dateRange.interval}
|
||||
provider={'infra-monitoring'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dateRange && (
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute={'max_cpu_usage'}
|
||||
label={'Max CPU usage'}
|
||||
interval={dateRange.interval}
|
||||
provider={'infra-monitoring'}
|
||||
/>
|
||||
)}
|
||||
{dateRange && (
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute={'max_cpu_usage'}
|
||||
label={'Max CPU usage'}
|
||||
interval={dateRange.interval}
|
||||
provider={'infra-monitoring'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dateRange && (
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute={'disk_io_consumption'}
|
||||
label={'Disk IO consumed'}
|
||||
interval={dateRange.interval}
|
||||
provider={'infra-monitoring'}
|
||||
/>
|
||||
)}
|
||||
{dateRange && (
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute={'disk_io_consumption'}
|
||||
label={'Disk IO consumed'}
|
||||
interval={dateRange.interval}
|
||||
provider={'infra-monitoring'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
|
||||
{dateRange && isReplicaSelected && (
|
||||
<Panel title="Replica Information">
|
||||
<Panel.Content>
|
||||
<div id="replication-lag">
|
||||
<ChartHandler
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
attribute="physical_replication_lag_physical_replica_lag_seconds"
|
||||
label="Replication lag"
|
||||
interval={dateRange.interval}
|
||||
provider="infra-monitoring"
|
||||
/>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<ReportWidget
|
||||
isLoading={report.isLoading}
|
||||
params={report.params.largeObjects}
|
||||
title="Database Size - Large Objects"
|
||||
data={report.data.largeObjects || []}
|
||||
queryType={'db'}
|
||||
resolvedSql={report.largeObjectsSql}
|
||||
renderer={(props) => {
|
||||
return (
|
||||
<div>
|
||||
<header className="text-sm">Database Size used</header>
|
||||
<p className="text-xl tracking-wide">{formatBytes(databaseSizeBytes)}</p>
|
||||
<ReportWidget
|
||||
isLoading={report.isLoading}
|
||||
params={report.params.largeObjects}
|
||||
title="Database Size - Large Objects"
|
||||
data={report.data.largeObjects || []}
|
||||
queryType={'db'}
|
||||
resolvedSql={report.largeObjectsSql}
|
||||
renderer={(props) => {
|
||||
return (
|
||||
<div>
|
||||
<header className="text-sm">Database Size used</header>
|
||||
<p className="text-xl tracking-wide">{formatBytes(databaseSizeBytes)}</p>
|
||||
|
||||
{!props.isLoading && props.data.length === 0 && (
|
||||
<span>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)
|
||||
{!props.isLoading && props.data.length === 0 && <span>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={() => (
|
||||
<div className="px-6 pb-2">
|
||||
<Alert_Shadcn_ variant="default" className="mt-4">
|
||||
<AlertDescription_Shadcn_>
|
||||
<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 type="default" icon={<IconExternalLink />}>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/platform/database-size#database-space-management"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Read about database space
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
append={() => (
|
||||
<div className="px-6 pb-2">
|
||||
<Alert_Shadcn_ variant="default" className="mt-4">
|
||||
<AlertDescription_Shadcn_>
|
||||
<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 type="default" icon={<IconExternalLink />}>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/platform/database-size#database-space-management"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Read about database space
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const useDatabaseReport = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const queryHooks = queriesFactory<keyof typeof PRESET_CONFIG.database.queries>(
|
||||
PRESET_CONFIG.database.queries,
|
||||
projectRef ?? 'default'
|
||||
)
|
||||
const largeObjects = queryHooks.largeObjects() as DbQueryHook
|
||||
const activeHooks = [largeObjects]
|
||||
|
||||
const isLoading = activeHooks.some((hook) => hook.isLoading)
|
||||
|
||||
return {
|
||||
data: {
|
||||
largeObjects: largeObjects.data,
|
||||
},
|
||||
errors: {
|
||||
largeObjects: largeObjects.error,
|
||||
},
|
||||
params: {
|
||||
largeObjects: largeObjects.params,
|
||||
},
|
||||
largeObjectsSql: largeObjects.resolvedSql,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user