diff --git a/apps/studio/components/interfaces/Reports/ReportHeader.tsx b/apps/studio/components/interfaces/Reports/ReportHeader.tsx index 65eee3aefe3..0f4ff52c5c5 100644 --- a/apps/studio/components/interfaces/Reports/ReportHeader.tsx +++ b/apps/studio/components/interfaces/Reports/ReportHeader.tsx @@ -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 = ({ title }) => ( -
-

{title}

-
-) +const ReportHeader = ({ title, showDatabaseSelector }: ReportHeaderProps) => { + const router = useRouter() + const { ref } = useParams() + const { db, chart, ...params } = router.query + + return ( +
+

{title}

+ {showDatabaseSelector && ( + { + router.push({ + pathname: router.pathname, + query: db !== ref ? { ...params, db } : params, + }) + }} + /> + )} +
+ ) +} export default ReportHeader diff --git a/apps/studio/components/interfaces/Reports/ReportPadding.tsx b/apps/studio/components/interfaces/Reports/ReportPadding.tsx index 5adf34a94a8..5e5f55096d9 100644 --- a/apps/studio/components/interfaces/Reports/ReportPadding.tsx +++ b/apps/studio/components/interfaces/Reports/ReportPadding.tsx @@ -1,7 +1,9 @@ +import { PropsWithChildren } from 'react' + /** * Standardized padding and width layout for non-custom reports */ -const ReportPadding: React.FC = ({ children }) => ( +const ReportPadding = ({ children }: PropsWithChildren<{}>) => (
{children}
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ChartConfig.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ChartConfig.tsx index b970ac05519..dc9febca068 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ChartConfig.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ChartConfig.tsx @@ -67,9 +67,7 @@ export function ChartConfig({ results = { rows: [] }, config, onConfigChange }:
) diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx index 3c504dc8a0d..f703f5f61d6 100644 --- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx @@ -169,7 +169,7 @@ export const DatabaseConnectionString = ({ appearance }: DatabaseConnectionStrin
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')} diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/Edge.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/Edge.tsx new file mode 100644 index 00000000000..90b751e2468 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/Edge.tsx @@ -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 ( + <> + + {data !== undefined && !isError && ( + + + +
+ {isLoading ? ( + + ) : ( +

{lagValue}s

+ )} +
+
+ + {isLoading + ? `Checking replication lag for replica ID: ${formattedId}` + : `Replication lag (seconds) for replica ID: ${formattedId}`} + +
+
+ )} + + ) +} diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx index fe6c905ab82..5a7db3ee91d 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx @@ -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 }} > diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx index b2e3c62918e..bfb96cd8d50 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx @@ -269,6 +269,14 @@ export const ReplicaNode = ({ data }: NodeProps) => { View connection string + + + View replication lag + + onSelectRestartReplica()}> Restart replica diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx index 561803d7d05..e11b25f62de 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx @@ -287,7 +287,16 @@ const MapView = ({ View connection string + + + View replication lag + + + + onSelectRestartReplica(database)} diff --git a/apps/studio/components/to-be-cleaned/Charts/ChartHandler.tsx b/apps/studio/components/to-be-cleaned/Charts/ChartHandler.tsx index 14e37c397fa..9148a3e5d00 100644 --- a/apps/studio/components/to-be-cleaned/Charts/ChartHandler.tsx +++ b/apps/studio/components/to-be-cleaned/Charts/ChartHandler.tsx @@ -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 (
@@ -152,30 +152,31 @@ const ChartHandler = ({
{!hideChartType && ( -
-
+ +
-
+ + + View as {chartStyle === 'bar' ? 'line chart' : 'bar chart'} + + )} {children}
- {chartStyle === 'bar' ? ( ) : ( 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 = ( -

- {label ?? attribute} -

- ) - const highlighted = ( -
- {title} - {!isByteAttribute && {format}} -
- ) - const date = ( -
- {focus ? ( - data && data[focus] && day(data[focus].period_start).format(FOCUS_FORMAT) - ) : ( - x - )} -
- ) - - if (minimalHeader) { - return ( -
- {chartTitle} -
- {highlighted} - {date} -
-
- ) - } - - 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(null) - const [mouseLeave, setMouseLeave] = useState(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 ( - -
-
-
- {hasData ? ( - <> - - { - // receives tooltip data https://github.com/recharts/recharts/blob/2a3405ff64a0c050d2cf94c36f0beef738d9e9c2/src/chart/generateCategoricalChart.tsx - if (onBarClick) onBarClick(tooltipData) - }} - > - - } /> - {/* */} - {/* */} - {yAxisLimit && } - - {data?.map((entry: any, index: any) => ( - - ))} - - - - {data && ( -
- - {day(data[0].period_start).format( - customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME - )} - - - {day(data[data?.length - 1]?.period_start).format( - customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME - )} - -
- )} - - ) : ( - - )} -
-
-
- ) -} diff --git a/apps/studio/components/ui/Charts/AreaChart.tsx b/apps/studio/components/ui/Charts/AreaChart.tsx index 4d6fcef6861..933999a0cf1 100644 --- a/apps/studio/components/ui/Charts/AreaChart.tsx +++ b/apps/studio/components/ui/Charts/AreaChart.tsx @@ -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 extends CommonChartProps { yAxisKey: string @@ -35,8 +34,6 @@ const AreaChart = ({ const { Container } = useChartSize(size) const [focusDataIndex, setFocusDataIndex] = useState(null) - if (data.length === 0) return - 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 ( + + ) + } + return (
extends CommonChartProps { yAxisKey: string @@ -43,9 +42,6 @@ const BarChart = ({ const { Container } = useChartSize(size) const [focusDataIndex, setFocusDataIndex] = useState(null) - if (data.length === 0) - return - 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 ( + + ) + } + return (
= ({ + +const ChartHeader = ({ format, highlightedValue, highlightedLabel, @@ -25,6 +26,7 @@ const ChartHeader: React.FC = ({ className={`text-foreground text-xl font-normal ${minimalHeader ? 'text-base' : 'text-2xl'}`} > {highlightedValue !== undefined && String(highlightedValue)} + {format === 'seconds' ? ' ' : ''} {typeof format === 'function' ? format(highlightedValue) : format} diff --git a/apps/studio/components/ui/Charts/NoDataPlaceholder.tsx b/apps/studio/components/ui/Charts/NoDataPlaceholder.tsx index 84c0d6c23bd..24f1a9950e4 100644 --- a/apps/studio/components/ui/Charts/NoDataPlaceholder.tsx +++ b/apps/studio/components/ui/Charts/NoDataPlaceholder.tsx @@ -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[0] } -const NoDataPlaceholder: React.FC = ({ - title = 'No data to show', - message, +const NoDataPlaceholder = ({ + attribute, + message = 'No data to show', + description, + format, className = '', size, -}) => { +}: NoDataPlaceholderProps) => { const { minHeight } = useChartSize(size) + return ( -
- -
-

{title}

- {message &&

{message}

} +
+ {attribute !== undefined && ( + + )} +
+ +
+

{message}

+ {description &&

{description}

} +
) diff --git a/apps/studio/components/ui/DatabaseSelector.tsx b/apps/studio/components/ui/DatabaseSelector.tsx index b4bc3067c78..14ac5a4c0c4 100644 --- a/apps/studio/components/ui/DatabaseSelector.tsx +++ b/apps/studio/components/ui/DatabaseSelector.tsx @@ -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' diff --git a/apps/studio/data/read-replicas/replica-lag-query.ts b/apps/studio/data/read-replicas/replica-lag-query.ts new file mode 100644 index 00000000000..423f7142754 --- /dev/null +++ b/apps/studio/data/read-replicas/replica-lag-query.ts @@ -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 = ( + { projectRef, connectionString, id }: ReplicationLagVariables, + { enabled, ...options }: UseQueryOptions = {} +) => + 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, + } + ) diff --git a/apps/studio/data/reports/database-report-query.ts b/apps/studio/data/reports/database-report-query.ts new file mode 100644 index 00000000000..0723c85337d --- /dev/null +++ b/apps/studio/data/reports/database-report-query.ts @@ -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( + 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, + } +} diff --git a/apps/studio/pages/project/[ref]/reports/database.tsx b/apps/studio/pages/project/[ref]/reports/database.tsx index 0f9f6b4f70b..bbcf2232386 100644 --- a/apps/studio/pages/project/[ref]/reports/database.tsx +++ b/apps/studio/pages/project/[ref]/reports/database.tsx @@ -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 ( -
-
-
- -
-
-
+ + + ) } @@ -38,10 +34,13 @@ DatabaseReport.getLayout = (page) => {page} { + const { db, chart } = useParams() const { project } = useProjectContext() const [dateRange, setDateRange] = useState(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 ( <> -
-
- -

Database health

- {showReadReplicasUI && } -
- } - > - -
- +
+ Database health}> + +
+ + {dateRange && ( +
+

+ {dayjs(dateRange.period_start.date).format('MMMM D, hh:mma')} +

+

+ +

+

+ {dayjs(dateRange.period_end.date).format('MMMM D, hh:mma')} +

+
+ )} +
+
+ {dateRange && ( + - {dateRange && ( -
-

- {dayjs(dateRange.period_start.date).format('MMMM D, hh:mma')} -

-

- -

-

- {dayjs(dateRange.period_end.date).format('MMMM D, hh:mma')} -

-
- )} -
-
- {dateRange && ( - - )} + )} - {dateRange && ( - - )} + {dateRange && ( + + )} - {dateRange && ( - - )} + {dateRange && ( + + )} - {dateRange && ( - - )} + {dateRange && ( + + )} - {dateRange && ( - - )} + {dateRange && ( + + )} +
+
+
+ + {dateRange && isReplicaSelected && ( + + +
+
+ )} - { - return ( -
-
Database Size used
-

{formatBytes(databaseSizeBytes)}

+ { + return ( +
+
Database Size used
+

{formatBytes(databaseSizeBytes)}

- {!props.isLoading && props.data.length === 0 && ( - No large objects found - )} - {!props.isLoading && props.data.length > 0 && ( - - Object - , - - Size - , - ]} - body={props.data?.map((object) => { - const percentage = ( - ((object.table_size as number) / databaseSizeBytes) * - 100 - ).toFixed(2) + {!props.isLoading && props.data.length === 0 && No large objects found} + {!props.isLoading && props.data.length > 0 && ( +
+ Object + , + + Size + , + ]} + body={props.data?.map((object) => { + const percentage = ( + ((object.table_size as number) / databaseSizeBytes) * + 100 + ).toFixed(2) - return ( - - - {object.schema_name}.{object.relname} - - - {formatBytes(object.table_size)} ({percentage}%) - - - ) - })} - /> - )} - - ) - }} - append={() => ( -
- - -
-

- 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. -

- - -
-
-
+ return ( + + + {object.schema_name}.{object.relname} + + + {formatBytes(object.table_size)} ({percentage}%) + + + ) + })} + /> + )}
- )} - /> - - + ) + }} + append={() => ( +
+ + +
+

+ 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. +

+ + +
+
+
+
+ )} + /> + ) } - -const useDatabaseReport = () => { - const { ref: projectRef } = useParams() - - const queryHooks = queriesFactory( - 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, - } -}