diff --git a/apps/docs/content/guides/platform/ssl-enforcement.mdx b/apps/docs/content/guides/platform/ssl-enforcement.mdx index 3bb6ebaf33239..2557347c6c63a 100644 --- a/apps/docs/content/guides/platform/ssl-enforcement.mdx +++ b/apps/docs/content/guides/platform/ssl-enforcement.mdx @@ -17,6 +17,12 @@ Projects need to be at least on Postgres 13.3.0 to enable SSL enforcement. You c SSL enforcement can be configured via the "Enforce SSL on incoming connections" setting under the SSL Configuration section in [Database Settings page](/dashboard/project/_/database/settings) of the dashboard. + + +Updating SSL enforcement requires a brief database reboot. This restarts only the database and involves a few minutes of downtime. + + + ## Manage SSL enforcement via the Management API You can also manage SSL enforcement using the Management API: diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx index 20ec963cd225c..1a0e8f30dbf41 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx @@ -245,23 +245,29 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo ) : ( - 'Never used' +

Never used

)} {x.expires_at ? ( dayjs(x.expires_at).isBefore(dayjs()) ? ( - + ) : ( ) ) : ( -

Never

+

Never

)}
diff --git a/apps/studio/components/interfaces/App/AppBannerWrapperContext.tsx b/apps/studio/components/interfaces/App/AppBannerWrapperContext.tsx deleted file mode 100644 index 30f005b9ac9de..0000000000000 --- a/apps/studio/components/interfaces/App/AppBannerWrapperContext.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { LOCAL_STORAGE_KEYS } from 'common' -import { noop } from 'lodash' -import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react' - -const MAINTENANCE_WINDOW_BANNER_KEY = LOCAL_STORAGE_KEYS.MAINTENANCE_WINDOW_BANNER - -// [Joshen] This file is meant to be dynamic - update this as and when we need to use the NoticeBanner - -type AppBannerContextType = { - maintenanceWindowBannerAcknowledged: boolean - onUpdateAcknowledged: (key: typeof MAINTENANCE_WINDOW_BANNER_KEY) => void -} - -const AppBannerContext = createContext({ - maintenanceWindowBannerAcknowledged: false, - onUpdateAcknowledged: noop, -}) - -export const useAppBannerContext = () => useContext(AppBannerContext) - -export const AppBannerContextProvider = ({ children }: PropsWithChildren<{}>) => { - const [maintenanceWindowBannerAcknowledged, setMaintenanceWindowBannerAcknowledged] = - useState(false) - - useEffect(() => { - if (typeof window !== 'undefined') { - const maintenanceAcknowledged = localStorage.getItem(MAINTENANCE_WINDOW_BANNER_KEY) === 'true' - setMaintenanceWindowBannerAcknowledged(maintenanceAcknowledged) - } - }, []) - - const value = { - maintenanceWindowBannerAcknowledged, - onUpdateAcknowledged: (key: typeof MAINTENANCE_WINDOW_BANNER_KEY) => { - if (key === MAINTENANCE_WINDOW_BANNER_KEY) { - if (typeof window !== 'undefined') { - window.localStorage.setItem(MAINTENANCE_WINDOW_BANNER_KEY, 'true') - } - setMaintenanceWindowBannerAcknowledged(true) - } - }, - } - - return {children} -} - -export const useIsNoticeBannerShown = () => { - const { maintenanceWindowBannerAcknowledged } = useAppBannerContext() - return maintenanceWindowBannerAcknowledged -} diff --git a/apps/studio/components/interfaces/Database/Replication/ReadReplicas/ReadReplicaDetails.tsx b/apps/studio/components/interfaces/Database/Replication/ReadReplicas/ReadReplicaDetails.tsx new file mode 100644 index 0000000000000..4ce4e316976b8 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/ReadReplicas/ReadReplicaDetails.tsx @@ -0,0 +1,180 @@ +import { BarChart2 } from 'lucide-react' +import { useMemo } from 'react' + +import { REPORT_DATERANGE_HELPER_LABELS } from '@/components/interfaces/Reports/Reports.constants' +import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants' +import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' +import { useInfraMonitoringAttributesQuery } from '@/data/analytics/infra-monitoring-query' +import { useLoadBalancersQuery } from '@/data/read-replicas/load-balancers-query' +import { useReplicationLagQuery } from '@/data/read-replicas/replica-lag-query' +import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' +import { useReadReplicasStatusesQuery } from '@/data/read-replicas/replicas-status-query' +import { useReportDateRange } from '@/hooks/misc/useReportDateRange' +import { BASE_PATH } from '@/lib/constants' +import { useFlag, useParams } from 'common' +import { AWS_REGIONS } from 'shared-data' +import { Card, CardContent, CardHeader, CardTitle } from 'ui' +import { + Chart, + ChartCard, + ChartContent, + ChartEmptyState, + ChartHeader, + ChartLine, + ChartLoadingState, + ChartMetric, + GenericSkeletonLoader, +} from 'ui-patterns' +import { Input } from 'ui-patterns/DataInputs/Input' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +export const ReadReplicaDetails = () => { + const { ref: projectRef, replicaId } = useParams() + const reportGranularityV2 = useFlag('reportGranularityV2') + + const { data = [], isPending: isLoadingDatabases } = useReadReplicasQuery({ projectRef }) + const replica = data.find((x) => x.identifier === replicaId) + const { identifier, connectionString, status: baseStatus, restUrl, region, size } = replica ?? {} + const regionLabel = Object.values(AWS_REGIONS).find((x) => x.code === region)?.displayName + + const { data: statuses = [] } = useReadReplicasStatusesQuery({ projectRef }) + const replicaStatus = statuses.find((x) => x.identifier === identifier) + const status = replicaStatus?.status ?? baseStatus + + const { data: loadBalancers = [] } = useLoadBalancersQuery({ projectRef }) + const loadBalancer = loadBalancers.find((x) => + x.databases.some((x) => x.identifier === identifier) + ) + + const { data: lagDuration, isPending: isLoadingLag } = useReplicationLagQuery( + { + id: identifier ?? '', + projectRef, + connectionString, + }, + { enabled: status === REPLICA_STATUS.ACTIVE_HEALTHY } + ) + + const { selectedDateRange } = useReportDateRange( + REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES, + reportGranularityV2 + ) + // [Joshen] This is unused but intentional to scaffold the usage for now, refer to comment below + const { data: infraMonitoringData, isPending: isFetchingInfraMonitoring } = + useInfraMonitoringAttributesQuery( + { + projectRef, + attributes: ['physical_replication_lag_physical_replica_lag_seconds'], + databaseIdentifier: identifier, + startDate: selectedDateRange.period_start.date, + endDate: selectedDateRange.period_end.date, + interval: selectedDateRange.interval, + }, + { enabled: !!replica } + ) + + // [Joshen] Temporarily hardcoding the data as the query to retrieve replication lag doesn't seem to be working + // https://linear.app/supabase/issue/INDATA-325/investigate-infra-monitoring-for-physical-replication-lag-physical + const chartData = useMemo(() => { + return Array.from({ length: 46 }, (_, i) => { + const date = new Date() + date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart + + return { + timestamp: date.toISOString(), + replication_lag: Math.floor(Math.random() * 100), + } + }).reverse() + }, []) + + return ( + <> + + + + + + } + title="No data to show" + description="It may take up to 24 hours for data to refresh" + /> + } + loadingState={} + > +
+ `${value}s`, + width: 80, + }} + isFullHeight={true} + /> +
+
+
+
+ + + + + Replica Information + + {isLoadingDatabases ? ( + + + + ) : ( + <> + + + + + + + + + + + + } + /> + + + + + + + )} + + + + + ) +} diff --git a/apps/studio/components/interfaces/Database/Replication/ReadReplicas/ReadReplicaRow.tsx b/apps/studio/components/interfaces/Database/Replication/ReadReplicas/ReadReplicaRow.tsx index c9845d622ad40..733c658b9bd3c 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReadReplicas/ReadReplicaRow.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReadReplicas/ReadReplicaRow.tsx @@ -1,15 +1,13 @@ import { Loader2, Minus, MoreVertical, RotateCcw, Trash } from 'lucide-react' +import Link from 'next/link' import { useMemo, useState } from 'react' -import DropReplicaConfirmationModal from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal' +import { DropReplicaConfirmationModal } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal' import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants' import { RestartReplicaConfirmationModal } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/RestartReplicaConfirmationModal' import { useReplicationLagQuery } from '@/data/read-replicas/replica-lag-query' import { type Database } from '@/data/read-replicas/replicas-query' -import { - DatabaseStatus, - ReplicaInitializationStatus, -} from '@/data/read-replicas/replicas-status-query' +import { DatabaseStatus } from '@/data/read-replicas/replicas-status-query' import { formatDatabaseID } from '@/data/read-replicas/replicas.utils' import { useParams } from 'common' import { Database as DatabaseIcon } from 'icons' @@ -26,6 +24,7 @@ import { TableRow, } from 'ui' import { ShimmeringLoader } from 'ui-patterns' +import { getIsInTransition, getStatusLabel } from './ReadReplicas.utils' interface ReadReplicaRow { replica: Database @@ -38,7 +37,7 @@ export const ReadReplicaRow = ({ replica, replicaStatus, onUpdateReplica }: Read const { identifier, region, status: baseStatus } = replica const status = replicaStatus?.status ?? baseStatus - const replicaInitializationStatus = replicaStatus?.replicaInitializationStatus + const initStatus = replicaStatus?.replicaInitializationStatus?.status const formattedId = formatDatabaseID(identifier ?? '') const { @@ -57,54 +56,13 @@ export const ReadReplicaRow = ({ replica, replicaStatus, onUpdateReplica }: Read const [showConfirmRestart, setShowConfirmRestart] = useState(false) const [showConfirmDrop, setShowConfirmDrop] = useState(false) - const initStatus = replicaInitializationStatus?.status const regionLabel = Object.values(AWS_REGIONS).find((x) => x.code === region)?.displayName - const isInTransition = - ( - [ - REPLICA_STATUS.UNKNOWN, - REPLICA_STATUS.COMING_UP, - REPLICA_STATUS.GOING_DOWN, - REPLICA_STATUS.RESTORING, - REPLICA_STATUS.RESTARTING, - REPLICA_STATUS.RESIZING, - REPLICA_STATUS.INIT_READ_REPLICA, - ] as string[] - ).includes(status) || initStatus === ReplicaInitializationStatus.InProgress - - const statusLabel = useMemo(() => { - if ( - initStatus === ReplicaInitializationStatus.InProgress || - status === REPLICA_STATUS.COMING_UP || - status === REPLICA_STATUS.UNKNOWN || - status === REPLICA_STATUS.INIT_READ_REPLICA - ) { - return 'Coming up' - } - - if ( - initStatus === ReplicaInitializationStatus.Failed || - status === REPLICA_STATUS.INIT_READ_REPLICA_FAILED - ) { - return 'Failed' - } - - switch (status) { - case REPLICA_STATUS.GOING_DOWN: - return 'Going down' - case REPLICA_STATUS.RESTARTING: - return 'Restarting' - case REPLICA_STATUS.RESIZING: - return 'Resizing' - case REPLICA_STATUS.RESTORING: - return 'Restoring' - case REPLICA_STATUS.ACTIVE_HEALTHY: - return 'Healthy' - default: - return 'Unhealthy' - } - }, [initStatus, status]) + const isInTransition = useMemo( + () => getIsInTransition({ initStatus, status }), + [initStatus, status] + ) + const statusLabel = useMemo(() => getStatusLabel({ initStatus, status }), [initStatus, status]) return ( <> @@ -153,21 +111,30 @@ export const ReadReplicaRow = ({ replica, replicaStatus, onUpdateReplica }: Read
- {/* [Joshen] Temporarily hidden - will work on the replica detail page in another PR */} - {/* */} +
+ } + subtitle={ + isLoadingDatabases ? ( + + ) : ( +
+ ID: {identifier} + +
+ ) + } + icon={ +
+ +
+ } + breadcrumbs={[ + { + label: 'Replication', + href: `/project/${ref}/database/replication`, + }, + { + label: `Read Replica - ${regionLabel}`, + }, + ]} + secondaryActions={ + } + tooltip={{ + content: { side: 'bottom', text: 'Drop replica' }, + }} + onClick={() => setShowConfirmDrop(true)} + /> + } + primaryActions={[ + , + , + ]} + > + + + router.push(`/project/${ref}/database/replication`)} + onCancel={() => setShowConfirmDrop(false)} + /> + + setStatusRefetchInterval(5000)} + onCancel={() => setShowConfirmRestart(false)} + /> + + ) +} + +DatabaseReadReplicaPage.getLayout = (page) => ( + + {page} + +) + +export default DatabaseReadReplicaPage diff --git a/apps/studio/pages/project/[ref]/logs/edge-logs.tsx b/apps/studio/pages/project/[ref]/logs/edge-logs.tsx index 89e7d1fa99c5d..155e0b27d0662 100644 --- a/apps/studio/pages/project/[ref]/logs/edge-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/edge-logs.tsx @@ -1,14 +1,14 @@ -import { useRouter } from 'next/router' - +import { useParams } from 'common' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import DefaultLayout from 'components/layouts/DefaultLayout' import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' +import { parseAsString, useQueryState } from 'nuqs' import type { NextPageWithLayout } from 'types' export const LogPage: NextPageWithLayout = () => { - const router = useRouter() - const { ref } = router.query + const { ref } = useParams() + const [identifier] = useQueryState('db', parseAsString) return ( { queryType="api" projectRef={ref as string} tableName={LogsTableName.EDGE} + filterOverride={!!identifier ? { identifier } : undefined} /> ) } diff --git a/apps/studio/pages/project/[ref]/logs/pooler-logs.tsx b/apps/studio/pages/project/[ref]/logs/pooler-logs.tsx index 4c2c298b58584..a6300e3ae3b0a 100644 --- a/apps/studio/pages/project/[ref]/logs/pooler-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/pooler-logs.tsx @@ -1,3 +1,5 @@ +import { parseAsString, useQueryState } from 'nuqs' + import { useParams } from 'common' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' @@ -9,6 +11,7 @@ import { LogoLoader } from 'ui' export const LogPage: NextPageWithLayout = () => { const { ref } = useParams() + const [identifier] = useQueryState('db', parseAsString) const { isPending: isLoading } = useSupavisorConfigurationQuery({ projectRef: ref ?? 'default' }) // this prevents initial load of pooler logs before config has been retrieved @@ -16,10 +19,11 @@ export const LogPage: NextPageWithLayout = () => { return ( ) } diff --git a/apps/studio/pages/project/[ref]/logs/postgres-logs.tsx b/apps/studio/pages/project/[ref]/logs/postgres-logs.tsx index a62e84164bab4..6cf3c10f3acbe 100644 --- a/apps/studio/pages/project/[ref]/logs/postgres-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/postgres-logs.tsx @@ -1,5 +1,6 @@ -import { useRouter } from 'next/router' +import { parseAsString, useQueryState } from 'nuqs' +import { useParams } from 'common' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import DefaultLayout from 'components/layouts/DefaultLayout' @@ -7,15 +8,16 @@ import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import type { NextPageWithLayout } from 'types' export const LogPage: NextPageWithLayout = () => { - const router = useRouter() - const { ref } = router.query + const { ref } = useParams() + const [identifier] = useQueryState('db', parseAsString) return ( ) } diff --git a/apps/studio/pages/project/[ref]/logs/postgrest-logs.tsx b/apps/studio/pages/project/[ref]/logs/postgrest-logs.tsx index af38c4c67853e..94001f79f72e5 100644 --- a/apps/studio/pages/project/[ref]/logs/postgrest-logs.tsx +++ b/apps/studio/pages/project/[ref]/logs/postgrest-logs.tsx @@ -1,5 +1,6 @@ -import { useRouter } from 'next/router' +import { parseAsString, useQueryState } from 'nuqs' +import { useParams } from 'common' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' import { LogsPreviewer } from 'components/interfaces/Settings/Logs/LogsPreviewer' import { LogsTableEmptyState } from 'components/interfaces/Settings/Logs/LogsTableEmptyState' @@ -8,8 +9,8 @@ import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import type { NextPageWithLayout } from 'types' export const LogPage: NextPageWithLayout = () => { - const router = useRouter() - const { ref } = router.query + const { ref } = useParams() + const [identifier] = useQueryState('db', parseAsString) return ( { description="Only errors are captured into PostgREST logs by default. Check the API Gateway logs for HTTP requests." /> } + filterOverride={!!identifier ? { identifier } : undefined} /> ) } diff --git a/apps/studio/pages/project/[ref]/observability/database.tsx b/apps/studio/pages/project/[ref]/observability/database.tsx index b764af9067042..48760a5591064 100644 --- a/apps/studio/pages/project/[ref]/observability/database.tsx +++ b/apps/studio/pages/project/[ref]/observability/database.tsx @@ -74,7 +74,6 @@ const DatabaseUsage = () => { updateDateRange, datePickerValue, datePickerHelpers, - isOrgPlanLoading, orgPlan, showUpgradePrompt, setShowUpgradePrompt, diff --git a/packages/ui-patterns/index.tsx b/packages/ui-patterns/index.tsx index 1bec804ac3423..1be06287121d5 100644 --- a/packages/ui-patterns/index.tsx +++ b/packages/ui-patterns/index.tsx @@ -8,6 +8,7 @@ export * from './src/admonition' export * from './src/AssistantChat/AssistantChatForm' export * from './src/AuthenticatedDropdownMenu' export * from './src/Banners' +export * from './src/Chart' export * from './src/CommandMenu' export * from './src/ComputeBadge' export * from './src/EmptyStatePresentational'