mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 23:59:08 +03:00
merge compute and disk
This commit is contained in:
1 parent
20290c71bd
commit
799e8d69d6
34 files changed
+1179
-1431
No files matched your search
@@ -276,7 +276,6 @@ These are the layout-only TanStack files. Most hold a single product layout comp
|
||||
- [x] A `routes/project/$ref/settings/general.tsx` ← `pages/project/[ref]/settings/general.tsx`
|
||||
- [x] A `routes/project/$ref/settings/addons.tsx` ← `pages/project/[ref]/settings/addons.tsx`
|
||||
- [x] A `routes/project/$ref/settings/api.tsx` ← `pages/project/[ref]/settings/api.tsx` (sets `skipSettingsLayout: true` — page is a useEffect redirect)
|
||||
- [x] A `routes/project/$ref/settings/compute-and-disk.tsx` ← `pages/project/[ref]/settings/compute-and-disk.tsx`
|
||||
- [x] A `routes/project/$ref/settings/dashboard.tsx` ← `pages/project/[ref]/settings/dashboard.tsx`
|
||||
- [x] A `routes/project/$ref/settings/infrastructure.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx`
|
||||
- [x] A `routes/project/$ref/settings/integrations.tsx` ← `pages/project/[ref]/settings/integrations.tsx`
|
||||
|
||||
@@ -522,9 +522,9 @@ export const CreateBranchModal = () => {
|
||||
<InlineLink
|
||||
onClick={() => setShowCreateBranchModal(false)}
|
||||
className="pointer-events-auto"
|
||||
href={`/project/${ref}/settings/compute-and-disk`}
|
||||
href={`/project/${ref}/settings/infrastructure`}
|
||||
>
|
||||
Compute and Disk
|
||||
Infrastructure
|
||||
</InlineLink>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ export const AdditionalMonthlySpend = ({
|
||||
be able to update the compute size and increase the disk size after the new project is
|
||||
created in{' '}
|
||||
<span className="font-mono text-xs tracking-tighter text-foreground-light">
|
||||
Project Settings > Compute and Disk
|
||||
Project Settings > Infrastructure
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 text-foreground mt-4">
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
import { useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { Activity, BarChart2, Database, ExternalLink } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import type { ChartConfig } from 'ui'
|
||||
import { cn } from 'ui'
|
||||
import {
|
||||
Chart,
|
||||
ChartActions,
|
||||
ChartCard,
|
||||
ChartContent,
|
||||
ChartEmptyState,
|
||||
ChartHeader,
|
||||
ChartLine,
|
||||
ChartLoadingState,
|
||||
ChartMetric,
|
||||
type ChartLineTick,
|
||||
} from 'ui-patterns/Chart'
|
||||
|
||||
import type { InfraMonitoringAttribute } from '@/data/analytics/infra-monitoring-query'
|
||||
import { useInfraMonitoringAttributesQuery } from '@/data/analytics/infra-monitoring-query'
|
||||
import { formatBytes } from '@/lib/helpers'
|
||||
|
||||
type UsageChartDatum = {
|
||||
timestamp: string
|
||||
maxCpuUsage?: number
|
||||
ramUsage?: number
|
||||
diskIoConsumption?: number
|
||||
databaseBytes?: number
|
||||
walBytes?: number
|
||||
systemBytes?: number
|
||||
diskSizeBytes?: number
|
||||
diskUsagePercent?: number
|
||||
}
|
||||
|
||||
const USAGE_ATTRIBUTES = [
|
||||
'max_cpu_usage',
|
||||
'ram_usage',
|
||||
'disk_io_consumption',
|
||||
'disk_fs_used_system',
|
||||
'disk_fs_used_wal',
|
||||
'pg_database_size',
|
||||
'disk_fs_size',
|
||||
] as InfraMonitoringAttribute[]
|
||||
|
||||
const COMPUTE_CHART_CONFIG = {
|
||||
maxCpuUsage: {
|
||||
label: 'CPU',
|
||||
color: 'hsl(var(--brand-default))',
|
||||
},
|
||||
ramUsage: {
|
||||
label: 'Memory',
|
||||
color: 'hsl(var(--warning-default))',
|
||||
},
|
||||
diskIoConsumption: {
|
||||
label: 'Disk IO',
|
||||
color: 'hsl(var(--destructive-500))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const DISK_CHART_CONFIG = {
|
||||
databaseUsagePercent: {
|
||||
label: 'Database',
|
||||
color: 'hsl(var(--brand-default))',
|
||||
},
|
||||
walUsagePercent: {
|
||||
label: 'WAL',
|
||||
color: 'hsl(var(--warning-default))',
|
||||
},
|
||||
systemUsagePercent: {
|
||||
label: 'System',
|
||||
color: 'hsl(var(--destructive-500))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const PERCENTAGE_Y_AXIS_PROPS = {
|
||||
domain: [0, 100] as [number, number],
|
||||
allowDataOverflow: true,
|
||||
ticks: [0, 25, 50, 75, 100],
|
||||
tickFormatter: (value: number) => `${Math.round(Number(value))}%`,
|
||||
width: 64,
|
||||
}
|
||||
|
||||
const formatUsagePercent = (value: number | undefined) =>
|
||||
value === undefined ? '—' : `${value.toFixed(0)}%`
|
||||
|
||||
const getUsageMetricStatus = (value: number | undefined): 'default' | 'warning' | 'negative' => {
|
||||
if (value === undefined) return 'default'
|
||||
if (value >= 90) return 'negative'
|
||||
if (value >= 75) return 'warning'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
const getWorstUsageMetricStatus = (
|
||||
...values: Array<number | undefined>
|
||||
): 'default' | 'warning' | 'negative' => {
|
||||
const statuses = values.map(getUsageMetricStatus)
|
||||
if (statuses.includes('negative')) return 'negative'
|
||||
if (statuses.includes('warning')) return 'warning'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
const getUsageCardClassName = (status: 'default' | 'warning' | 'negative') =>
|
||||
cn(
|
||||
'h-full flex flex-col transition-colors',
|
||||
status === 'warning' && 'border-warning-400 bg-warning-200/30',
|
||||
status === 'negative' && 'border-destructive-400 bg-destructive-200/30'
|
||||
)
|
||||
|
||||
const toNumber = (value: string | number | undefined) => {
|
||||
const parsedValue = Number(value)
|
||||
return Number.isFinite(parsedValue) ? parsedValue : 0
|
||||
}
|
||||
|
||||
const clampPercentage = (value: number) => Math.min(Math.max(value, 0), 100)
|
||||
|
||||
const getPeakChartValue = (data: UsageChartDatum[], dataKey: keyof UsageChartDatum) => {
|
||||
if (data.length === 0) return undefined
|
||||
|
||||
const values = data
|
||||
.map((point) => point[dataKey])
|
||||
.filter((value): value is number => typeof value === 'number')
|
||||
|
||||
if (values.length === 0) return undefined
|
||||
|
||||
return Math.max(...values)
|
||||
}
|
||||
|
||||
export const ComputeAndDiskUsageCharts = ({ className }: { className?: string }) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
// Intentionally anchored to mount time so the query key stays stable across re-renders.
|
||||
const { startDate, endDate } = useMemo(() => {
|
||||
const now = dayjs()
|
||||
|
||||
return {
|
||||
startDate: now.subtract(7, 'day').toISOString(),
|
||||
endDate: now.toISOString(),
|
||||
}
|
||||
}, [])
|
||||
|
||||
const {
|
||||
data: usageData,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useInfraMonitoringAttributesQuery({
|
||||
projectRef,
|
||||
attributes: USAGE_ATTRIBUTES,
|
||||
startDate,
|
||||
endDate,
|
||||
interval: '1d',
|
||||
})
|
||||
|
||||
const { computeChartData, diskChartData } = useMemo(() => {
|
||||
if (!usageData || !('series' in usageData)) {
|
||||
return {
|
||||
computeChartData: [],
|
||||
diskChartData: [],
|
||||
}
|
||||
}
|
||||
|
||||
const computeChartData = usageData.data.map((point) => ({
|
||||
timestamp: point.period_start,
|
||||
maxCpuUsage: clampPercentage(toNumber(point.values.max_cpu_usage)),
|
||||
ramUsage: clampPercentage(toNumber(point.values.ram_usage)),
|
||||
diskIoConsumption: clampPercentage(toNumber(point.values.disk_io_consumption)),
|
||||
}))
|
||||
|
||||
const diskChartData = usageData.data.flatMap((point) => {
|
||||
const databaseBytes = toNumber(point.values.pg_database_size)
|
||||
const walBytes = toNumber(point.values.disk_fs_used_wal)
|
||||
const systemBytes = toNumber(point.values.disk_fs_used_system)
|
||||
const totalBytes = toNumber(point.values.disk_fs_size)
|
||||
if (totalBytes <= 0) return []
|
||||
|
||||
return {
|
||||
timestamp: point.period_start,
|
||||
databaseBytes,
|
||||
walBytes,
|
||||
systemBytes,
|
||||
diskSizeBytes: totalBytes,
|
||||
databaseUsagePercent: clampPercentage((databaseBytes / totalBytes) * 100),
|
||||
walUsagePercent: clampPercentage((walBytes / totalBytes) * 100),
|
||||
systemUsagePercent: clampPercentage((systemBytes / totalBytes) * 100),
|
||||
}
|
||||
})
|
||||
|
||||
return { computeChartData, diskChartData }
|
||||
}, [usageData])
|
||||
|
||||
const peakCpuUsage = getPeakChartValue(computeChartData, 'maxCpuUsage')
|
||||
const peakMemoryUsage = getPeakChartValue(computeChartData, 'ramUsage')
|
||||
const peakDiskIoUsage = getPeakChartValue(computeChartData, 'diskIoConsumption')
|
||||
const computePeaks = [peakCpuUsage, peakMemoryUsage, peakDiskIoUsage].filter(
|
||||
(value): value is number => value !== undefined
|
||||
)
|
||||
const peakComputeUsage = computePeaks.length > 0 ? Math.max(...computePeaks) : undefined
|
||||
const computeUsageStatus = getWorstUsageMetricStatus(
|
||||
peakCpuUsage,
|
||||
peakMemoryUsage,
|
||||
peakDiskIoUsage
|
||||
)
|
||||
|
||||
const latestDiskDataPoint = diskChartData[diskChartData.length - 1]
|
||||
const latestDiskUsedBytes =
|
||||
(latestDiskDataPoint?.databaseBytes ?? 0) +
|
||||
(latestDiskDataPoint?.walBytes ?? 0) +
|
||||
(latestDiskDataPoint?.systemBytes ?? 0)
|
||||
const latestDiskSizeBytes = latestDiskDataPoint?.diskSizeBytes ?? 0
|
||||
const latestDiskUsage =
|
||||
latestDiskSizeBytes > 0
|
||||
? clampPercentage((latestDiskUsedBytes / latestDiskSizeBytes) * 100)
|
||||
: undefined
|
||||
const diskUsageStatus = getUsageMetricStatus(latestDiskUsage)
|
||||
|
||||
const databaseReportUrl = `/project/${projectRef}/observability/database`
|
||||
const chartActions = [
|
||||
{
|
||||
label: 'View database report',
|
||||
href: databaseReportUrl,
|
||||
icon: <ExternalLink size={12} />,
|
||||
},
|
||||
]
|
||||
|
||||
const isComputeEmpty = computeChartData.length === 0
|
||||
const isDiskEmpty = diskChartData.length === 0
|
||||
|
||||
return (
|
||||
<div className={cn('grid grid-cols-1 gap-4 @xl:grid-cols-2 @xl:items-stretch', className)}>
|
||||
<div id="cpu" className="scroll-mt-24 h-full min-h-0">
|
||||
<Chart isLoading={isLoading} isErrored={isError} className="h-full">
|
||||
<ChartCard className={getUsageCardClassName(computeUsageStatus)}>
|
||||
<ChartHeader align="start" className="min-h-[5.5rem]">
|
||||
<ChartMetric
|
||||
label="Compute utilization"
|
||||
tooltip="Peak CPU, memory, and disk IO usage over the last 7 days. Sustained high usage may require a larger compute size."
|
||||
value={formatUsagePercent(peakComputeUsage)}
|
||||
status={computeUsageStatus}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-6">
|
||||
<ChartMetric
|
||||
label="CPU"
|
||||
tooltip="Highest daily CPU usage"
|
||||
value={formatUsagePercent(peakCpuUsage)}
|
||||
status={getUsageMetricStatus(peakCpuUsage)}
|
||||
align="end"
|
||||
/>
|
||||
<ChartMetric
|
||||
label="Memory"
|
||||
tooltip="Highest daily memory usage"
|
||||
value={formatUsagePercent(peakMemoryUsage)}
|
||||
status={getUsageMetricStatus(peakMemoryUsage)}
|
||||
align="end"
|
||||
/>
|
||||
<ChartMetric
|
||||
label="Disk IO"
|
||||
tooltip="Highest daily disk IO consumption"
|
||||
value={formatUsagePercent(peakDiskIoUsage)}
|
||||
status={getUsageMetricStatus(peakDiskIoUsage)}
|
||||
align="end"
|
||||
/>
|
||||
<ChartActions actions={chartActions} />
|
||||
</div>
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
className="p-0"
|
||||
isEmpty={isComputeEmpty}
|
||||
loadingState={<ChartLoadingState className="h-48 border-x-0 border-b-0" />}
|
||||
errorState={
|
||||
<ChartEmptyState
|
||||
className="h-48 border-x-0 border-b-0"
|
||||
icon={<Activity size={16} />}
|
||||
title="Failed to load usage data"
|
||||
/>
|
||||
}
|
||||
emptyState={
|
||||
<ChartEmptyState
|
||||
className="h-48 border-x-0 border-b-0"
|
||||
icon={<Activity size={16} />}
|
||||
title="No compute data"
|
||||
description="Usage data may take a few minutes to appear."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="h-48 px-card pb-4 pt-2">
|
||||
<ChartLine
|
||||
data={computeChartData as ChartLineTick[]}
|
||||
dataKey="maxCpuUsage"
|
||||
dataKeys={['maxCpuUsage', 'ramUsage', 'diskIoConsumption']}
|
||||
DateTimeFormat="MMM D"
|
||||
isFullHeight
|
||||
showGrid
|
||||
showYAxis
|
||||
config={COMPUTE_CHART_CONFIG}
|
||||
YAxisProps={PERCENTAGE_Y_AXIS_PROPS}
|
||||
/>
|
||||
</div>
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
</div>
|
||||
|
||||
<div id="disk" className="scroll-mt-24 h-full min-h-0">
|
||||
<Chart isLoading={isLoading} isErrored={isError} className="h-full">
|
||||
<ChartCard className={getUsageCardClassName(diskUsageStatus)}>
|
||||
<ChartHeader align="start" className="min-h-[5.5rem]">
|
||||
<ChartMetric
|
||||
label="Disk usage"
|
||||
tooltip="Provisioned disk and used disk split by database, WAL, and system usage over the last 7 days."
|
||||
value={formatUsagePercent(latestDiskUsage)}
|
||||
status={diskUsageStatus}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-6">
|
||||
<ChartMetric
|
||||
label="Database"
|
||||
tooltip="Database storage used"
|
||||
value={formatBytes(latestDiskDataPoint?.databaseBytes ?? 0, 1)}
|
||||
align="end"
|
||||
/>
|
||||
<ChartMetric
|
||||
label="WAL"
|
||||
tooltip="Write-ahead log storage used"
|
||||
value={formatBytes(latestDiskDataPoint?.walBytes ?? 0, 1)}
|
||||
align="end"
|
||||
/>
|
||||
<ChartMetric
|
||||
label="System"
|
||||
tooltip="System storage used"
|
||||
value={formatBytes(latestDiskDataPoint?.systemBytes ?? 0, 1)}
|
||||
align="end"
|
||||
/>
|
||||
<ChartActions actions={chartActions} />
|
||||
</div>
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
className="p-0"
|
||||
isEmpty={isDiskEmpty}
|
||||
loadingState={<ChartLoadingState className="h-48 border-x-0 border-b-0" />}
|
||||
errorState={
|
||||
<ChartEmptyState
|
||||
className="h-48 border-x-0 border-b-0"
|
||||
icon={<BarChart2 size={16} />}
|
||||
title="Failed to load usage data"
|
||||
/>
|
||||
}
|
||||
emptyState={
|
||||
<ChartEmptyState
|
||||
className="h-48 border-x-0 border-b-0"
|
||||
icon={<Database size={16} />}
|
||||
title="No disk data"
|
||||
description="Disk metrics may take a few minutes to appear."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="h-48 px-card pb-4 pt-2">
|
||||
<ChartLine
|
||||
data={diskChartData as ChartLineTick[]}
|
||||
dataKey="databaseUsagePercent"
|
||||
dataKeys={['databaseUsagePercent', 'walUsagePercent', 'systemUsagePercent']}
|
||||
DateTimeFormat="MMM D"
|
||||
isFullHeight
|
||||
showGrid
|
||||
showYAxis
|
||||
config={DISK_CHART_CONFIG}
|
||||
YAxisProps={PERCENTAGE_Y_AXIS_PROPS}
|
||||
/>
|
||||
</div>
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,24 +2,24 @@ import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { CloudProvider } from 'shared-data'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
DialogSectionSeparator,
|
||||
Form,
|
||||
Separator,
|
||||
} from 'ui'
|
||||
import { Button, Card, CardContent, cn, Form } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { PageContainer } from 'ui-patterns/PageContainer'
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionAside,
|
||||
PageSectionContent,
|
||||
PageSectionDescription,
|
||||
PageSectionMeta,
|
||||
PageSectionSummary,
|
||||
PageSectionTitle,
|
||||
} from 'ui-patterns/PageSection'
|
||||
|
||||
import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge'
|
||||
import { ComputeAndDiskUsageCharts } from './ComputeAndDiskUsageCharts'
|
||||
import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagement.schema'
|
||||
import { DiskManagementMessage } from './DiskManagement.types'
|
||||
import {
|
||||
@@ -28,12 +28,18 @@ import {
|
||||
} from './DiskManagement.utils'
|
||||
import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection'
|
||||
import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog'
|
||||
import { useDiskManagementReviewChanges } from './DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks'
|
||||
import { AutoScaleFields } from './fields/AutoScaleFields'
|
||||
import { ComputeSizeField } from './fields/ComputeSizeField'
|
||||
import {
|
||||
ComputeSectionBillingBadge,
|
||||
ComputeSizeField,
|
||||
ComputeSizeFieldMeta,
|
||||
} from './fields/ComputeSizeField'
|
||||
import { DiskSizeField } from './fields/DiskSizeField'
|
||||
import { IOPSField } from './fields/IOPSField'
|
||||
import { StorageTypeField } from './fields/StorageTypeField'
|
||||
import { ThroughputField } from './fields/ThroughputField'
|
||||
import { BillingChangeBadge } from './ui/BillingChangeBadge'
|
||||
import { DiskCountdownRadial } from './ui/DiskCountdownRadial'
|
||||
import {
|
||||
DISK_LIMITS,
|
||||
@@ -41,12 +47,9 @@ import {
|
||||
PLAN_DETAILS,
|
||||
RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3,
|
||||
} from './ui/DiskManagement.constants'
|
||||
import { NoticeBar } from './ui/NoticeBar'
|
||||
import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection'
|
||||
import {
|
||||
MAX_WIDTH_CLASSES,
|
||||
PADDING_CLASSES,
|
||||
ScaffoldContainer,
|
||||
} from '@/components/layouts/Scaffold'
|
||||
import { PADDING_CLASSES } from '@/components/layouts/Scaffold'
|
||||
import { DocsButton } from '@/components/ui/DocsButton'
|
||||
import { RequestUpgradeToBillingOwners } from '@/components/ui/RequestUpgradeToBillingOwners'
|
||||
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
|
||||
@@ -75,13 +78,14 @@ import {
|
||||
} from '@/hooks/misc/useSelectedProject'
|
||||
import { DOCS_URL, GB, PROJECT_STATUS } from '@/lib/constants'
|
||||
|
||||
export function DiskManagementForm() {
|
||||
export function DiskManagementForm({ chartsClassName }: { chartsClassName?: string } = {}) {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: project, isPending: isProjectPending } = useSelectedProjectQuery()
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
const { setProjectStatus } = useSetProjectStatus()
|
||||
|
||||
const advancedSettingsRef = useRef<HTMLDivElement>(null)
|
||||
const autoscaleSettingsRef = useRef<HTMLDivElement>(null)
|
||||
const storageSettingsRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isSpendCapEnabled =
|
||||
org?.plan.id !== 'free' && !org?.usage_billing_enabled && project?.cloud_provider !== 'FLY'
|
||||
@@ -110,7 +114,6 @@ export function DiskManagementForm() {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false)
|
||||
const [refetchInterval, setRefetchInterval] = useState<number | false>(false)
|
||||
const [message, setMessageState] = useState<DiskManagementMessage | null>(null)
|
||||
const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false)
|
||||
|
||||
const { data: databases, isSuccess: isReadReplicasSuccess } = useReadReplicasQuery({ projectRef })
|
||||
const { data, isSuccess: isDiskAttributesSuccess } = useDiskAttributesQuery(
|
||||
@@ -183,6 +186,18 @@ export function DiskManagementForm() {
|
||||
const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef)
|
||||
const isPlanUpgradeRequired = !hasAccess
|
||||
|
||||
const {
|
||||
computeSizePrice,
|
||||
diskSizePrice,
|
||||
totalBeforePrice,
|
||||
totalAfterPrice,
|
||||
advancedBeforePrice,
|
||||
advancedAfterPrice,
|
||||
showComputeBillingBadge,
|
||||
showDiskBillingBadge,
|
||||
showAdvancedBillingBadge,
|
||||
} = useDiskManagementReviewChanges(form, readReplicas.length)
|
||||
|
||||
const { formState } = form
|
||||
const errors = formState.errors
|
||||
const usedSize = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100
|
||||
@@ -345,7 +360,7 @@ export function DiskManagementForm() {
|
||||
form.setValue('provisionedIOPS', DISK_LIMITS['gp3'].minIops)
|
||||
}
|
||||
}
|
||||
}, [modifiedComputeSize, isDialogOpen, project])
|
||||
}, [modifiedComputeSize, form, isDialogOpen, project])
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize field values properly when data has been loaded, preserving any user changes
|
||||
@@ -357,218 +372,247 @@ export function DiskManagementForm() {
|
||||
|
||||
useEffect(() => {
|
||||
const fieldErrors = Object.keys(errors)
|
||||
if (fieldErrors.length > 0) {
|
||||
if (
|
||||
fieldErrors.includes('throughput') ||
|
||||
fieldErrors.includes('provisionedIOPS') ||
|
||||
fieldErrors.includes('maxSizeGb')
|
||||
) {
|
||||
setAdvancedSettingsOpenState(true)
|
||||
if (fieldErrors.length === 0) return
|
||||
|
||||
// [Joshen] The timeout is to let the collapsible open prior to scrolling
|
||||
const timeoutId = setTimeout(() => {
|
||||
advancedSettingsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}, 100)
|
||||
const scrollTarget =
|
||||
fieldErrors.includes('maxSizeGb') ||
|
||||
fieldErrors.includes('growthPercent') ||
|
||||
fieldErrors.includes('minIncrementGb')
|
||||
? autoscaleSettingsRef
|
||||
: fieldErrors.includes('throughput') || fieldErrors.includes('provisionedIOPS')
|
||||
? storageSettingsRef
|
||||
: null
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
if (!scrollTarget) return
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
scrollTarget.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}, 100)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}, [errors])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
|
||||
{isEntitlementsLoaded && isPlanUpgradeRequired && (
|
||||
<UpgradeToPro
|
||||
featureProposition="configure compute and disk"
|
||||
primaryText="Only available on Pro Plan and above"
|
||||
secondaryText="Upgrade to the Pro Plan to configure compute and disk settings."
|
||||
/>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form id="disk-compute-form" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<PageContainer size="default" className="pb-16">
|
||||
<ComputeAndDiskUsageCharts className={chartsClassName} />
|
||||
|
||||
{(isProjectResizing ||
|
||||
isProjectRequestingDiskChanges ||
|
||||
(isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
|
||||
<div className="relative flex flex-col gap-10">
|
||||
<DiskMangementRestartRequiredSection
|
||||
visible={isProjectResizing}
|
||||
title="Your project will now automatically restart."
|
||||
description="Your project will be unavailable for up to 2 mins."
|
||||
{isEntitlementsLoaded && isPlanUpgradeRequired && (
|
||||
<UpgradeToPro
|
||||
featureProposition="configure compute and disk"
|
||||
primaryText="Only available on Pro Plan and above"
|
||||
secondaryText="Upgrade to the Pro Plan to configure compute and disk settings."
|
||||
/>
|
||||
{isProjectRequestingDiskChanges && (
|
||||
<Admonition
|
||||
)}
|
||||
|
||||
{(isProjectResizing ||
|
||||
isProjectRequestingDiskChanges ||
|
||||
(isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
|
||||
<div className="relative flex flex-col gap-10">
|
||||
<DiskMangementRestartRequiredSection
|
||||
visible={isProjectResizing}
|
||||
title="Your project will now automatically restart."
|
||||
description="Your project will be unavailable for up to 2 mins."
|
||||
/>
|
||||
<NoticeBar
|
||||
type="default"
|
||||
layout="horizontal"
|
||||
visible={isProjectRequestingDiskChanges}
|
||||
title="Disk configuration changes have been requested"
|
||||
description="The requested changes will be applied to your disk shortly"
|
||||
/>
|
||||
)}
|
||||
{isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions && (
|
||||
<Admonition
|
||||
<NoticeBar
|
||||
type="default"
|
||||
layout="horizontal"
|
||||
visible={isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions}
|
||||
title="You do not have permission to update disk configuration"
|
||||
description="Please contact your organization administrator to update your disk configuration"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
</ScaffoldContainer>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="disk-compute-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-8"
|
||||
>
|
||||
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
|
||||
<ComputeSizeField form={form} disabled={disableComputeInputs} />
|
||||
|
||||
{isDiskNoticeVisible && <Separator />}
|
||||
|
||||
<SpendCapDisabledSection currentDiskSizeGb={defaultValues.totalSize} />
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{isDiskNoticeVisible && (
|
||||
<Admonition
|
||||
type="default"
|
||||
layout="horizontal"
|
||||
title="Disk configuration is only available for projects in the AWS cloud provider"
|
||||
description={
|
||||
isAwsK8s
|
||||
? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
|
||||
: isBranch
|
||||
? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
|
||||
: 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isAws && (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<DiskCountdownRadial />
|
||||
{!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
|
||||
<Admonition
|
||||
type="destructive"
|
||||
title="Database size is currently over 90% of disk size"
|
||||
description="Your project will enter read-only mode once you reach 95% of the disk space to prevent your database from exceeding the disk limitations"
|
||||
>
|
||||
<DocsButton
|
||||
abbrev={false}
|
||||
className="mt-2"
|
||||
href={`${DOCS_URL}/guides/platform/database-size#read-only-mode`}
|
||||
/>
|
||||
</Admonition>
|
||||
)}
|
||||
{isReadOnlyMode && (
|
||||
<Admonition
|
||||
type="destructive"
|
||||
title="Project is currently in read-only mode"
|
||||
description="You will need to manually override read-only mode and reduce the database size to below 95% of the disk size"
|
||||
>
|
||||
<DocsButton
|
||||
abbrev={false}
|
||||
className="mt-2"
|
||||
href={`${DOCS_URL}/guides/platform/database-size#disabling-read-only-mode`}
|
||||
/>
|
||||
</Admonition>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DiskSizeField
|
||||
form={form}
|
||||
disableInput={disableDiskSizeInput}
|
||||
setAdvancedSettingsOpenState={setAdvancedSettingsOpenState}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAws && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
<Collapsible
|
||||
open={advancedSettingsOpen}
|
||||
onOpenChange={() => setAdvancedSettingsOpenState((prev) => !prev)}
|
||||
>
|
||||
<CollapsibleTrigger className="px-card py-3 w-full border flex items-center gap-6 rounded-t data-closed:rounded-b group justify-between">
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="text-sm text-foreground">Advanced disk settings</span>
|
||||
<span className="text-sm text-foreground-light text-left">
|
||||
Specify additional settings for your disk, including autoscaling
|
||||
configuration, IOPS, throughput, and disk type.
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight
|
||||
size={16}
|
||||
className="text-foreground-light transition-all group-data-open:rotate-90"
|
||||
strokeWidth={1}
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Scaling</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<PageSection className="pt-0">
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle className="heading-default text-base">
|
||||
Compute size
|
||||
</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
<ComputeSizeFieldMeta />
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
<PageSectionAside>
|
||||
<ComputeSectionBillingBadge
|
||||
form={form}
|
||||
show={showComputeBillingBadge}
|
||||
beforePrice={Number(computeSizePrice.oldPrice)}
|
||||
afterPrice={Number(computeSizePrice.newPrice)}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent
|
||||
ref={advancedSettingsRef}
|
||||
className={cn(
|
||||
'transition-all rounded-b',
|
||||
'border border-t-0 data-closed:animate-collapsible-up data-open:animate-collapsible-down'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-y-8 py-8">
|
||||
<div className="px-card flex flex-col gap-y-8">
|
||||
<AutoScaleFields form={form} />
|
||||
</div>
|
||||
<DialogSectionSeparator />
|
||||
<div className="px-card flex flex-col gap-y-8">
|
||||
{!!disableIopsThroughputConfig && (
|
||||
<DocsButton href={`${DOCS_URL}/guides/platform/compute-and-disk`} />
|
||||
</PageSectionAside>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<ComputeSizeField form={form} disabled={disableComputeInputs} />
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
|
||||
<PageSection id="disk-size">
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle className="heading-default text-base">Disk</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
Configure provisioned storage for your primary database.
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
<PageSectionAside>
|
||||
<BillingChangeBadge
|
||||
show={showDiskBillingBadge}
|
||||
beforePrice={Number(diskSizePrice.oldPrice)}
|
||||
afterPrice={Number(diskSizePrice.newPrice)}
|
||||
/>
|
||||
<DocsButton href={`${DOCS_URL}/guides/platform/database-size`} />
|
||||
</PageSectionAside>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent className="flex flex-col gap-4">
|
||||
<SpendCapDisabledSection currentDiskSizeGb={defaultValues.totalSize} />
|
||||
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={isDiskNoticeVisible}
|
||||
title="Disk configuration is only available for projects in the AWS cloud provider"
|
||||
description={
|
||||
isAwsK8s
|
||||
? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
|
||||
: isBranch
|
||||
? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
|
||||
: 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
|
||||
}
|
||||
/>
|
||||
|
||||
{isAws && (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<DiskCountdownRadial />
|
||||
{!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
|
||||
<Admonition
|
||||
type="default"
|
||||
title="Adjusting disk configuration requires LARGE Compute size or above"
|
||||
description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
|
||||
actions={
|
||||
canUpdateDiskConfiguration ? (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
form.setValue('computeSize', 'ci_large')
|
||||
}}
|
||||
>
|
||||
Change to LARGE Compute
|
||||
</Button>
|
||||
) : (
|
||||
<RequestUpgradeToBillingOwners
|
||||
addon="computeSize"
|
||||
featureProposition="adjust disk configuration"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isDiskTooSmallForCustomIops &&
|
||||
!disableIopsThroughputConfig &&
|
||||
!disableDiskInputs && (
|
||||
<Admonition
|
||||
type="default"
|
||||
title="Increase disk size to adjust IOPS or throughput"
|
||||
description={`This disk is too small to update IOPS or throughput, since gp3 volumes are capped at 500 IOPS per GB with a 3,000 IOPS minimum. Resizing to ${suggestedDiskSizeForCustomIops} GB unlocks custom IOPS and throughput, and leaves headroom for further adjustments (disk config changes are locked for 4 hours after each resize).`}
|
||||
actions={
|
||||
!disableDiskSizeInput ? (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
form.setValue('totalSize', suggestedDiskSizeForCustomIops, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Increase to {suggestedDiskSizeForCustomIops} GB
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
type="destructive"
|
||||
title="Database size is currently over 90% of disk size"
|
||||
description="Your project will enter read-only mode once you reach 95% of the disk space to prevent your database from exceeding the disk limitations"
|
||||
>
|
||||
<DocsButton
|
||||
abbrev={false}
|
||||
className="mt-2"
|
||||
href={`${DOCS_URL}/guides/platform/database-size#read-only-mode`}
|
||||
/>
|
||||
)}
|
||||
</Admonition>
|
||||
)}
|
||||
{isReadOnlyMode && (
|
||||
<Admonition
|
||||
type="destructive"
|
||||
title="Project is currently in read-only mode"
|
||||
description="You will need to manually override read-only mode and reduce the database size to below 95% of the disk size"
|
||||
>
|
||||
<DocsButton
|
||||
abbrev={false}
|
||||
className="mt-2"
|
||||
href={`${DOCS_URL}/guides/platform/database-size#disabling-read-only-mode`}
|
||||
/>
|
||||
</Admonition>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<DiskSizeField form={form} disableInput={disableDiskSizeInput} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
|
||||
{isAws && (
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle className="heading-default text-base">
|
||||
Advanced
|
||||
</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
Configure autoscaling, storage type, IOPS, and throughput.
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
<PageSectionAside>
|
||||
<BillingChangeBadge
|
||||
show={showAdvancedBillingBadge}
|
||||
beforePrice={advancedBeforePrice}
|
||||
afterPrice={advancedAfterPrice}
|
||||
/>
|
||||
</PageSectionAside>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent className="flex flex-col gap-4">
|
||||
<Card ref={autoscaleSettingsRef} className="scroll-mt-24">
|
||||
<CardContent className="flex flex-col gap-y-8">
|
||||
<AutoScaleFields form={form} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card ref={storageSettingsRef} className="scroll-mt-24">
|
||||
<CardContent className="flex flex-col gap-y-8">
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={!!disableIopsThroughputConfig}
|
||||
title="Adjusting disk configuration requires LARGE Compute size or above"
|
||||
description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
|
||||
actions={
|
||||
canUpdateDiskConfiguration ? (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
form.setValue('computeSize', 'ci_large')
|
||||
}}
|
||||
>
|
||||
Change to LARGE Compute
|
||||
</Button>
|
||||
) : (
|
||||
<RequestUpgradeToBillingOwners
|
||||
addon="computeSize"
|
||||
featureProposition="adjust disk configuration"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={
|
||||
isDiskTooSmallForCustomIops &&
|
||||
!disableIopsThroughputConfig &&
|
||||
!disableDiskInputs
|
||||
}
|
||||
title={`Increase disk size to adjust IOPS or throughput`}
|
||||
description={`This disk is too small to update IOPS or throughput, since gp3 volumes are capped at 500 IOPS per GB with a 3,000 IOPS minimum. Resizing to ${suggestedDiskSizeForCustomIops} GB unlocks custom IOPS and throughput, and leaves headroom for further adjustments (disk config changes are locked for 4 hours after each resize).`}
|
||||
actions={
|
||||
!disableDiskSizeInput ? (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
form.setValue('totalSize', suggestedDiskSizeForCustomIops, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Increase to {suggestedDiskSizeForCustomIops} GB
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<StorageTypeField
|
||||
form={form}
|
||||
disableInput={disableIopsThroughputConfig || disableDiskInputs}
|
||||
@@ -589,55 +633,59 @@ export function DiskManagementForm() {
|
||||
isDiskTooSmallForCustomIops
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</>
|
||||
)}
|
||||
</ScaffoldContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
</PageContainer>
|
||||
|
||||
<AnimatePresence>
|
||||
{isDirty ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.1, delay: 0.2 }}
|
||||
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
|
||||
<AnimatePresence>
|
||||
{isDirty ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.1, delay: 0.2 }}
|
||||
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mx-auto w-full max-w-[1200px]',
|
||||
PADDING_CLASSES,
|
||||
'flex items-center justify-end gap-3'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
MAX_WIDTH_CLASSES,
|
||||
PADDING_CLASSES,
|
||||
'flex items-center gap-3 justify-end'
|
||||
)}
|
||||
<BillingChangeBadge
|
||||
show={isDirty}
|
||||
beforePrice={totalBeforePrice}
|
||||
afterPrice={totalAfterPrice}
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => form.reset()}
|
||||
disabled={!isDirty}
|
||||
size="medium"
|
||||
>
|
||||
<FormFooterChangeBadge formState={formState} />
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => form.reset()}
|
||||
disabled={!isDirty}
|
||||
size="medium"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<DiskManagementReviewAndSubmitDialog
|
||||
loading={isUpdatingConfig}
|
||||
disabled={noPermissions}
|
||||
form={form}
|
||||
numReplicas={readReplicas.length}
|
||||
isDialogOpen={isDialogOpen}
|
||||
onSubmit={onSubmit}
|
||||
setIsDialogOpen={setIsDialogOpen}
|
||||
message={message}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
Cancel
|
||||
</Button>
|
||||
<DiskManagementReviewAndSubmitDialog
|
||||
loading={isUpdatingConfig}
|
||||
disabled={noPermissions}
|
||||
form={form}
|
||||
numReplicas={readReplicas.length}
|
||||
isDialogOpen={isDialogOpen}
|
||||
onSubmit={onSubmit}
|
||||
setIsDialogOpen={setIsDialogOpen}
|
||||
message={message}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -30,11 +30,11 @@ export function DiskManagementPanelForm() {
|
||||
type="default"
|
||||
layout="responsive"
|
||||
title="Disk Management has moved"
|
||||
description="Disk configuration is now managed alongside Project Compute on the new Compute and Disk page."
|
||||
description="Disk configuration is now managed alongside Project Compute on the Infrastructure page."
|
||||
actions={
|
||||
<Button variant="default" asChild>
|
||||
<Link href={`/project/${projectRef}/settings/compute-and-disk`}>
|
||||
Go to Compute and Disk
|
||||
<Link href={`/project/${projectRef}/settings/infrastructure`}>
|
||||
Go to Infrastructure
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
|
||||
+25
@@ -22,6 +22,8 @@ export function useDiskManagementReviewChanges(
|
||||
form: UseFormReturn<DiskStorageSchemaType>,
|
||||
numReplicas: number
|
||||
) {
|
||||
form.watch(['computeSize', 'totalSize', 'storageType', 'provisionedIOPS', 'throughput'])
|
||||
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
const isAwsNimbus = useIsAwsNimbusCloudProvider()
|
||||
@@ -78,6 +80,24 @@ export function useDiskManagementReviewChanges(
|
||||
Number(iopsPrice.newPrice) +
|
||||
Number(throughputPrice.newPrice)
|
||||
|
||||
const advancedBeforePrice = Number(iopsPrice.oldPrice) + Number(throughputPrice.oldPrice)
|
||||
const advancedAfterPrice = Number(iopsPrice.newPrice) + Number(throughputPrice.newPrice)
|
||||
|
||||
const { isDirty, dirtyFields, errors } = form.formState
|
||||
|
||||
const showComputeBillingBadge = isDirty && !!dirtyFields.computeSize && !errors.computeSize
|
||||
|
||||
const showDiskBillingBadge =
|
||||
isDirty &&
|
||||
!errors.totalSize &&
|
||||
Number(diskSizePrice.oldPrice) !== Number(diskSizePrice.newPrice)
|
||||
|
||||
const showAdvancedBillingBadge =
|
||||
isDirty &&
|
||||
advancedBeforePrice !== advancedAfterPrice &&
|
||||
!errors.provisionedIOPS &&
|
||||
!errors.throughput
|
||||
|
||||
// --- Change flags ---
|
||||
|
||||
const hasComputeChanges =
|
||||
@@ -166,6 +186,11 @@ export function useDiskManagementReviewChanges(
|
||||
throughputPrice,
|
||||
totalBeforePrice,
|
||||
totalAfterPrice,
|
||||
advancedBeforePrice,
|
||||
advancedAfterPrice,
|
||||
showComputeBillingBadge,
|
||||
showDiskBillingBadge,
|
||||
showAdvancedBillingBadge,
|
||||
// change flags
|
||||
hasComputeChanges,
|
||||
hasTotalSizeChanges,
|
||||
|
||||
@@ -55,7 +55,7 @@ export const AutoScaleFields = ({ form }: AutoScaleFieldProps) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
layout="flex-row-reverse"
|
||||
label="Autoscale growth percent"
|
||||
id={field.name}
|
||||
labelOptional="Percentage of current disk size to grow"
|
||||
@@ -101,7 +101,7 @@ export const AutoScaleFields = ({ form }: AutoScaleFieldProps) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
layout="flex-row-reverse"
|
||||
label="Minimum increment"
|
||||
id={field.name}
|
||||
labelOptional="Minimum value to autoscale disk size by"
|
||||
@@ -147,7 +147,7 @@ export const AutoScaleFields = ({ form }: AutoScaleFieldProps) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
layout="flex-row-reverse"
|
||||
label="Maximum disk size"
|
||||
id={field.name}
|
||||
labelOptional="Maximum size that the disk can grow to"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { SupportCategories } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { ChevronRight, CpuIcon, Lock, Microchip } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { CpuIcon, Lock, Microchip } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { UseFormReturn } from 'react-hook-form'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
FormField,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
RadioGroupCard,
|
||||
RadioGroupCardItem,
|
||||
Skeleton,
|
||||
@@ -14,20 +16,14 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { Admonition, ComputeBadge } from 'ui-patterns'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { ComputeBadge } from 'ui-patterns'
|
||||
|
||||
import { DiskStorageSchemaType } from '../DiskManagement.schema'
|
||||
import { ComputeInstanceAddonVariantId, InfraInstanceSize } from '../DiskManagement.types'
|
||||
import {
|
||||
calculateComputeSizePrice,
|
||||
ComputeAddonVariant,
|
||||
getAvailableComputeOptions,
|
||||
} from '../DiskManagement.utils'
|
||||
import { ComputeAddonVariant, getAvailableComputeOptions } from '../DiskManagement.utils'
|
||||
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
|
||||
import FormMessage from '../ui/FormMessage'
|
||||
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
|
||||
import { DocsButton } from '@/components/ui/DocsButton'
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
|
||||
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
||||
@@ -35,7 +31,6 @@ import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { getCloudProviderArchitecture } from '@/lib/cloudprovider-utils'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
const INITIALLY_VISIBLE_COUNT = 6
|
||||
|
||||
@@ -47,6 +42,42 @@ type ComputeSizeFieldProps = {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function ComputeSizeFieldMeta() {
|
||||
return <p>Hardware resources allocated to your Postgres database</p>
|
||||
}
|
||||
|
||||
type ComputeSectionBillingBadgeProps = {
|
||||
form: UseFormReturn<DiskStorageSchemaType>
|
||||
show: boolean
|
||||
beforePrice: number
|
||||
afterPrice: number
|
||||
}
|
||||
|
||||
export function ComputeSectionBillingBadge({
|
||||
form,
|
||||
show,
|
||||
beforePrice,
|
||||
afterPrice,
|
||||
}: ComputeSectionBillingBadgeProps) {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { hasAccess: entitledUpdateCompute } = useCheckEntitlements(
|
||||
'instances.compute_update_available_sizes'
|
||||
)
|
||||
|
||||
const computeSize = form.watch('computeSize')
|
||||
const projectComputeSize = project?.infra_compute_size ?? 'nano'
|
||||
const showUpgradeBadge = entitledUpdateCompute && projectComputeSize === 'nano'
|
||||
|
||||
return (
|
||||
<BillingChangeBadge
|
||||
show={show}
|
||||
beforePrice={beforePrice}
|
||||
afterPrice={afterPrice}
|
||||
free={showUpgradeBadge && computeSize === 'ci_micro'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
|
||||
const { ref } = useParams()
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
@@ -57,8 +88,6 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
|
||||
|
||||
const showComputePrice = useIsFeatureEnabled('project_addons:show_compute_price')
|
||||
|
||||
const { computeSize } = form.watch()
|
||||
|
||||
const {
|
||||
data: addons,
|
||||
isPending: isAddonsLoading,
|
||||
@@ -67,7 +96,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
|
||||
|
||||
const isLoading = isProjectLoading || isAddonsLoading || isEntitlementLoading
|
||||
|
||||
const { control, formState, setValue, trigger } = form
|
||||
const { control, setValue, trigger } = form
|
||||
|
||||
const availableAddons = useMemo(() => {
|
||||
return addons?.available_addons ?? []
|
||||
@@ -81,44 +110,11 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
|
||||
return getAvailableComputeOptions(availableAddons, project?.cloud_provider)
|
||||
}, [availableAddons, project?.cloud_provider])
|
||||
|
||||
// Expand by default if the project's current compute size is beyond the initial visible set
|
||||
const [showAllSizes, setShowAllSizes] = useState(() => {
|
||||
const idx = availableOptions.findIndex((o) => o.identifier === computeSize)
|
||||
return idx >= INITIALLY_VISIBLE_COUNT
|
||||
})
|
||||
|
||||
// Expand whenever the selected size falls outside the visible set — covers both initial data
|
||||
// load (availableOptions starts empty) and computeSize changes after mount (e.g. form reset)
|
||||
useEffect(() => {
|
||||
const idx = availableOptions.findIndex((o) => o.identifier === computeSize)
|
||||
if (idx >= INITIALLY_VISIBLE_COUNT) {
|
||||
setShowAllSizes(true)
|
||||
}
|
||||
}, [computeSize, availableOptions])
|
||||
|
||||
const subscriptionPitr = addons?.selected_addons.find((addon) => addon.type === 'pitr')
|
||||
|
||||
const computeSizePrice = calculateComputeSizePrice({
|
||||
availableOptions: availableOptions,
|
||||
oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro',
|
||||
newComputeSize: form.getValues('computeSize'),
|
||||
plan: org?.plan.id ?? 'free',
|
||||
})
|
||||
|
||||
const projectComputeSize = project?.infra_compute_size ?? 'nano'
|
||||
const showUpgradeBadge = entitledUpdateCompute && projectComputeSize === 'nano'
|
||||
|
||||
const selectedOptionIndex = availableOptions.findIndex((o) => o.identifier === computeSize)
|
||||
const selectedOptionIsHidden = selectedOptionIndex >= INITIALLY_VISIBLE_COUNT
|
||||
|
||||
// Always show all options if the selected one would be outside the visible slice,
|
||||
// so the active card is never hidden from the user.
|
||||
const visibleOptions =
|
||||
showAllSizes || selectedOptionIsHidden
|
||||
? availableOptions
|
||||
: availableOptions.slice(0, INITIALLY_VISIBLE_COUNT)
|
||||
const hasHiddenOptions = availableOptions.length > INITIALLY_VISIBLE_COUNT
|
||||
|
||||
return (
|
||||
<FormField
|
||||
name="computeSize"
|
||||
@@ -137,267 +133,224 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
|
||||
defaultValue={field.value}
|
||||
disabled={disabled}
|
||||
>
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label="Compute size"
|
||||
id={field.name}
|
||||
className="gap-5"
|
||||
labelOptional={
|
||||
<>
|
||||
<BillingChangeBadge
|
||||
className="mb-2"
|
||||
show={
|
||||
formState.isDirty &&
|
||||
formState.dirtyFields.computeSize &&
|
||||
!formState.errors.computeSize
|
||||
}
|
||||
beforePrice={Number(computeSizePrice.oldPrice)}
|
||||
afterPrice={Number(computeSizePrice.newPrice)}
|
||||
free={showUpgradeBadge && computeSize === 'ci_micro' ? true : false}
|
||||
/>
|
||||
<p className="text-foreground-lighter">
|
||||
Hardware resources allocated to your Postgres database
|
||||
</p>
|
||||
|
||||
<div className="mt-3">
|
||||
<DocsButton
|
||||
abbrev={false}
|
||||
href={`${DOCS_URL}/guides/platform/compute-and-disk`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showUpgradeBadge && form.watch('computeSize') === 'ci_nano' && (
|
||||
<Admonition
|
||||
showIcon={false}
|
||||
type="default"
|
||||
className="mt-3 border-violet-900 bg-violet-200 [&_h5]:text-violet-1100"
|
||||
title="Upgrade to Micro Compute"
|
||||
description="This Project is already paying for Micro Compute. You can upgrade to Micro Compute at any time when convenient."
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<div
|
||||
className={
|
||||
!addonsError
|
||||
? 'grid gap-4 grid-cols-[repeat(auto-fit,minmax(min(100%,13em),1fr))]'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
!addonsError
|
||||
? 'grid gap-4 grid-cols-[repeat(auto-fit,minmax(min(100%,13em),1fr))]'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
Array(INITIALLY_VISIBLE_COUNT)
|
||||
.fill(0)
|
||||
.map((_, i) => <Skeleton key={i} className="w-full h-[110px] rounded-md" />)
|
||||
) : addonsError ? (
|
||||
<FormMessage message={'Failed to load Compute size options'} type="error">
|
||||
<p>{addonsError?.message}</p>
|
||||
</FormMessage>
|
||||
) : (
|
||||
<>
|
||||
{visibleOptions.map((compute) => {
|
||||
const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider)
|
||||
{isLoading ? (
|
||||
Array(INITIALLY_VISIBLE_COUNT)
|
||||
.fill(0)
|
||||
.map((_, i) => <Skeleton key={i} className="w-full h-[110px] rounded-md" />)
|
||||
) : addonsError ? (
|
||||
<FormMessage message={'Failed to load Compute size options'} type="error">
|
||||
<p>{addonsError?.message}</p>
|
||||
</FormMessage>
|
||||
) : (
|
||||
<>
|
||||
{availableOptions.map((compute) => {
|
||||
const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider)
|
||||
|
||||
const lockedMicroDueToPITR =
|
||||
compute.identifier === 'ci_micro' && !!subscriptionPitr
|
||||
const lockedNanoDueToPlan =
|
||||
org?.plan.id !== 'free' &&
|
||||
project?.infra_compute_size !== 'nano' &&
|
||||
compute.identifier === 'ci_nano'
|
||||
const lockedMicroDueToPITR =
|
||||
compute.identifier === 'ci_micro' && !!subscriptionPitr
|
||||
const lockedNanoDueToPlan =
|
||||
org?.plan.id !== 'free' &&
|
||||
project?.infra_compute_size !== 'nano' &&
|
||||
compute.identifier === 'ci_nano'
|
||||
|
||||
const lockedOption = lockedNanoDueToPlan || lockedMicroDueToPITR
|
||||
const lockedOption = lockedNanoDueToPlan || lockedMicroDueToPITR
|
||||
|
||||
const price =
|
||||
org?.plan.id !== 'free' &&
|
||||
project?.infra_compute_size === 'nano' &&
|
||||
compute.identifier === 'ci_nano'
|
||||
? availableOptions.find(
|
||||
(option: ComputeAddonVariant) => option.identifier === 'ci_micro'
|
||||
)?.price
|
||||
: compute.price
|
||||
const price =
|
||||
org?.plan.id !== 'free' &&
|
||||
project?.infra_compute_size === 'nano' &&
|
||||
compute.identifier === 'ci_nano'
|
||||
? availableOptions.find(
|
||||
(option: ComputeAddonVariant) => option.identifier === 'ci_micro'
|
||||
)?.price
|
||||
: compute.price
|
||||
|
||||
const cpuLabel = (() => {
|
||||
const cpuCores = compute.meta?.cpu_cores
|
||||
if (typeof cpuCores === 'number') {
|
||||
return `${cpuCores}-core ${cpuArchitecture} CPU`
|
||||
}
|
||||
if (cpuCores) {
|
||||
return `${cpuCores} CPU`
|
||||
}
|
||||
return 'CPU'
|
||||
})()
|
||||
const cpuLabel = (() => {
|
||||
const cpuCores = compute.meta?.cpu_cores
|
||||
if (typeof cpuCores === 'number') {
|
||||
return `${cpuCores}-core ${cpuArchitecture} CPU`
|
||||
}
|
||||
if (cpuCores) {
|
||||
return `${cpuCores} CPU`
|
||||
}
|
||||
return 'CPU'
|
||||
})()
|
||||
|
||||
return (
|
||||
<RadioGroupCardItem
|
||||
showIndicator={false}
|
||||
id={compute.identifier}
|
||||
key={compute.identifier}
|
||||
value={compute.identifier}
|
||||
className={cn(
|
||||
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]',
|
||||
lockedOption && 'opacity-50'
|
||||
)}
|
||||
disabled={disabled || lockedOption}
|
||||
label={
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
{showUpgradeBadge && compute.identifier === 'ci_micro' && (
|
||||
<div className="absolute -top-4 -right-3 text-violet-1100 flex items-center gap-1 bg-surface-75 py-0.5 px-2 rounded-full border border-violet-900">
|
||||
<span>No additional charge</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full flex flex-col gap-3 justify-between">
|
||||
<div className="relative px-3 opacity-50 group-data-checked:opacity-100 flex justify-between">
|
||||
<ComputeBadge
|
||||
className="inline-flex font-semibold"
|
||||
infraComputeSize={compute.name as InfraInstanceSize}
|
||||
/>
|
||||
<div className="flex items-center space-x-1">
|
||||
{lockedOption ? (
|
||||
<div className="bg border rounded-lg h-7 w-7 flex items-center justify-center">
|
||||
<Lock size={14} />
|
||||
</div>
|
||||
) : (
|
||||
showComputePrice && (
|
||||
<>
|
||||
<span
|
||||
className="text-foreground text-sm font-semibold"
|
||||
translate="no"
|
||||
>
|
||||
${price}
|
||||
</span>
|
||||
<span className="text-foreground-light translate-y-px">
|
||||
{' '}
|
||||
/{' '}
|
||||
{compute.price_interval === 'monthly'
|
||||
? 'month'
|
||||
: 'hour'}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<div className="px-3 text-sm flex flex-col gap-1">
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<Microchip
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>
|
||||
{compute.identifier === 'ci_nano' && 'Up to '}
|
||||
{compute.meta?.memory_gb ?? 0} GB memory
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<CpuIcon
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>{cpuLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{lockedMicroDueToPITR && (
|
||||
<TooltipContent side="bottom" className="w-64 text-center">
|
||||
Project has PITR enabled which requires a minimum of Small compute.
|
||||
Please{' '}
|
||||
<InlineLink href="/project/_/settings/addons?panel=pitr">
|
||||
disable PITR
|
||||
</InlineLink>{' '}
|
||||
first before selecting Micro
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{showAllSizes && (
|
||||
return (
|
||||
<RadioGroupCardItem
|
||||
id="larger-compute"
|
||||
key="larger-compute"
|
||||
showIndicator={false}
|
||||
value="larger-compute"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
id={compute.identifier}
|
||||
key={compute.identifier}
|
||||
value={compute.identifier}
|
||||
className={cn(
|
||||
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]'
|
||||
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]',
|
||||
lockedOption && 'opacity-50'
|
||||
)}
|
||||
disabled={disabled || lockedOption}
|
||||
label={
|
||||
<SupportLink
|
||||
queryParams={{
|
||||
projectRef: ref,
|
||||
category: SupportCategories.SALES_ENQUIRY,
|
||||
subject: 'Enquiry about larger instance sizes',
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex flex-col gap-3 justify-between">
|
||||
<div className="relative px-3 flex justify-between">
|
||||
<ComputeBadge infraComputeSize=">16XL" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
{showUpgradeBadge && compute.identifier === 'ci_micro' && (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div
|
||||
className="absolute -top-4 -right-3 text-violet-1100 flex items-center gap-1 bg-surface-75 py-0.5 px-2 rounded-full border border-violet-900 cursor-default"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span>Free Upgrade</span>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
side="top"
|
||||
align="end"
|
||||
className="w-72"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
Upgrade to Micro Compute
|
||||
</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
This Project is already paying for Micro Compute. You can
|
||||
upgrade to Micro Compute at any time when convenient.
|
||||
</p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)}
|
||||
<div className="w-full flex flex-col gap-3 justify-between">
|
||||
<div className="relative px-3 opacity-50 group-data-checked:opacity-100 flex justify-between">
|
||||
<ComputeBadge
|
||||
className="inline-flex font-semibold"
|
||||
infraComputeSize={compute.name as InfraInstanceSize}
|
||||
/>
|
||||
<div className="flex items-center space-x-1">
|
||||
{lockedOption ? (
|
||||
<div className="bg border rounded-lg h-7 w-7 flex items-center justify-center">
|
||||
<Lock size={14} />
|
||||
</div>
|
||||
) : (
|
||||
showComputePrice && (
|
||||
<>
|
||||
<span
|
||||
className="text-foreground text-sm font-semibold"
|
||||
translate="no"
|
||||
>
|
||||
${price}
|
||||
</span>
|
||||
<span className="text-foreground-light translate-y-px">
|
||||
{' '}
|
||||
/{' '}
|
||||
{compute.price_interval === 'monthly'
|
||||
? 'month'
|
||||
: 'hour'}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1 opacity-50 ">
|
||||
<span className="text-foreground-light text-sm">Contact Us</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="px-3 text-sm flex flex-col gap-1">
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<Microchip
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>Custom memory</span>
|
||||
</div>
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<CpuIcon
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>Custom CPU</span>
|
||||
<div className="w-full">
|
||||
<div className="px-3 text-sm flex flex-col gap-1">
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<Microchip
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>
|
||||
{compute.identifier === 'ci_nano' && 'Up to '}
|
||||
{compute.meta?.memory_gb ?? 0} GB memory
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<CpuIcon
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>{cpuLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SupportLink>
|
||||
</TooltipTrigger>
|
||||
{lockedMicroDueToPITR && (
|
||||
<TooltipContent side="bottom" className="w-64 text-center">
|
||||
Project has PITR enabled which requires a minimum of Small compute.
|
||||
Please{' '}
|
||||
<InlineLink href="/project/_/settings/addons?panel=pitr">
|
||||
disable PITR
|
||||
</InlineLink>{' '}
|
||||
first before selecting Micro
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{!isLoading && !addonsError && hasHiddenOptions && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="tiny"
|
||||
className="mt-4"
|
||||
aria-expanded={showAllSizes}
|
||||
// Prevent collapsing when the selected size would become hidden
|
||||
disabled={showAllSizes && selectedOptionIsHidden}
|
||||
onClick={() => setShowAllSizes((prev) => !prev)}
|
||||
icon={
|
||||
<ChevronRight
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
className={cn('transition-transform', showAllSizes && '-rotate-90')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{showAllSizes ? 'Show fewer sizes' : 'Show all sizes'}
|
||||
</Button>
|
||||
<RadioGroupCardItem
|
||||
id="larger-compute"
|
||||
key="larger-compute"
|
||||
showIndicator={false}
|
||||
value="larger-compute"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
className={cn(
|
||||
'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]'
|
||||
)}
|
||||
label={
|
||||
<SupportLink
|
||||
queryParams={{
|
||||
projectRef: ref,
|
||||
category: SupportCategories.SALES_ENQUIRY,
|
||||
subject: 'Enquiry about larger instance sizes',
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex flex-col gap-3 justify-between">
|
||||
<div className="relative px-3 flex justify-between">
|
||||
<ComputeBadge infraComputeSize=">16XL" />
|
||||
|
||||
<div className="flex items-center space-x-1 opacity-50 ">
|
||||
<span className="text-foreground-light text-sm">Contact Us</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="px-3 text-sm flex flex-col gap-1">
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<Microchip
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>Custom memory</span>
|
||||
</div>
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<CpuIcon
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>Custom CPU</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SupportLink>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FormItemLayout>
|
||||
</div>
|
||||
</RadioGroupCard>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -15,31 +15,22 @@ import {
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
import { DiskStorageSchemaType } from '../DiskManagement.schema'
|
||||
import { calculateDiskSizePrice } from '../DiskManagement.utils'
|
||||
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
|
||||
import { DiskType, PLAN_DETAILS } from '../ui/DiskManagement.constants'
|
||||
import { DiskManagementDiskSizeReadReplicas } from '../ui/DiskManagementReadReplicas'
|
||||
import { DiskSpaceBar } from '../ui/DiskSpaceBar'
|
||||
import { DiskTypeRecommendationSection } from '../ui/DiskTypeRecommendationSection'
|
||||
import FormMessage from '../ui/FormMessage'
|
||||
import { DocsButton } from '@/components/ui/DocsButton'
|
||||
import { useDiskAttributesQuery } from '@/data/config/disk-attributes-query'
|
||||
import { useDiskUtilizationQuery } from '@/data/config/disk-utilization-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { DOCS_URL, GB } from '@/lib/constants'
|
||||
import { GB } from '@/lib/constants'
|
||||
|
||||
type DiskSizeFieldProps = {
|
||||
form: UseFormReturn<DiskStorageSchemaType>
|
||||
disableInput: boolean
|
||||
setAdvancedSettingsOpenState: (state: boolean) => void
|
||||
}
|
||||
|
||||
export function DiskSizeField({
|
||||
form,
|
||||
disableInput,
|
||||
setAdvancedSettingsOpenState,
|
||||
}: DiskSizeFieldProps) {
|
||||
export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { control, formState, setValue, trigger, getValues, resetField, watch } = form
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
@@ -79,125 +70,107 @@ export function DiskSizeField({
|
||||
PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS] ?? {}
|
||||
const includedDiskGB = includedDiskGBMeta[watchedStorageType]
|
||||
|
||||
const { defaultValues, dirtyFields, isDirty, errors } = formState
|
||||
const diskSizePrice = calculateDiskSizePrice({
|
||||
planId,
|
||||
oldSize: defaultValues?.totalSize || 0,
|
||||
oldStorageType: defaultValues?.storageType as DiskType,
|
||||
newSize: getValues('totalSize'),
|
||||
newStorageType: getValues('storageType') as DiskType,
|
||||
})
|
||||
const { defaultValues, dirtyFields } = formState
|
||||
|
||||
const mainDiskUsed = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100
|
||||
|
||||
return (
|
||||
<div id="disk-size" className="grid @xl:grid-cols-12 gap-5">
|
||||
<div className="col-span-4">
|
||||
<FormField
|
||||
name="totalSize"
|
||||
control={control}
|
||||
render={({ field, fieldState: { isDirty } }) => (
|
||||
<FormItemLayout label="Disk Size" layout="vertical" id={field.name}>
|
||||
<FormControl className="max-w-32">
|
||||
<InputGroup>
|
||||
<FormInputGroupInput
|
||||
type="number"
|
||||
id={field.name}
|
||||
{...field}
|
||||
disabled={disableInput || isError}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
onChange={(e) => {
|
||||
setValue('totalSize', e.target.valueAsNumber, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
<FormField
|
||||
name="totalSize"
|
||||
control={control}
|
||||
render={({ field, fieldState: { isDirty } }) => (
|
||||
<FormItemLayout
|
||||
label="Disk size"
|
||||
layout="flex-row-reverse"
|
||||
id={field.name}
|
||||
description={
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{includedDiskGB > 0 && org?.plan.id && (
|
||||
<p>
|
||||
Your plan includes up to {includedDiskGB} GB of {watchedStorageType} storage.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<DiskTypeRecommendationSection
|
||||
form={form}
|
||||
actions={
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setValue('storageType', 'io2')
|
||||
trigger('provisionedIOPS')
|
||||
trigger('throughput')
|
||||
trigger('totalSize')
|
||||
}}
|
||||
min={includedDiskGB}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>GB</InputGroupText>
|
||||
{isDirty ? (
|
||||
<InputGroupButton
|
||||
type="button"
|
||||
variant="default"
|
||||
size="tiny"
|
||||
className="px-2 text-foreground-light"
|
||||
onClick={() => {
|
||||
resetField('totalSize')
|
||||
trigger('provisionedIOPS')
|
||||
}}
|
||||
title="Reset"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
||||
</InputGroupButton>
|
||||
) : null}
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<BillingChangeBadge
|
||||
className="mt-1"
|
||||
beforePrice={Number(diskSizePrice.oldPrice)}
|
||||
afterPrice={Number(diskSizePrice.newPrice)}
|
||||
show={isDirty && !errors.totalSize && diskSizePrice.oldPrice !== diskSizePrice.newPrice}
|
||||
/>
|
||||
<span className="text-foreground-lighter text-sm">
|
||||
{includedDiskGB > 0 &&
|
||||
org?.plan.id &&
|
||||
`Your plan includes up to ${includedDiskGB} GB of ${watchedStorageType} storage.`}
|
||||
>
|
||||
Change to High Performance SSD
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mt-3">
|
||||
<DocsButton abbrev={false} href={`${DOCS_URL}/guides/platform/database-size`} />
|
||||
{isProjectNew ? (
|
||||
<FormMessage
|
||||
message="Disk size data is not available for ~10 minutes after project creation"
|
||||
type="error"
|
||||
/>
|
||||
) : (
|
||||
error && (
|
||||
<FormMessage message="Failed to load disk size data" type="error">
|
||||
{error?.message}
|
||||
</FormMessage>
|
||||
)
|
||||
)}
|
||||
|
||||
<DiskManagementDiskSizeReadReplicas
|
||||
isDirty={dirtyFields.totalSize !== undefined}
|
||||
totalSize={(defaultValues?.totalSize || 0) * 1.25}
|
||||
usedSize={mainDiskUsed}
|
||||
newTotalSize={watchedTotalSize * 1.25}
|
||||
oldStorageType={defaultValues?.storageType as DiskType}
|
||||
newStorageType={getValues('storageType') as DiskType}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<DiskTypeRecommendationSection
|
||||
form={form}
|
||||
actions={
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setValue('storageType', 'io2')
|
||||
}
|
||||
>
|
||||
<FormControl className="max-w-32">
|
||||
<InputGroup>
|
||||
<FormInputGroupInput
|
||||
type="number"
|
||||
id={field.name}
|
||||
{...field}
|
||||
disabled={disableInput || isError}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
onChange={(e) => {
|
||||
setValue('totalSize', e.target.valueAsNumber, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
trigger('provisionedIOPS')
|
||||
trigger('totalSize')
|
||||
setAdvancedSettingsOpenState(true)
|
||||
trigger('throughput')
|
||||
}}
|
||||
>
|
||||
Change to High Performance SSD
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-8">
|
||||
<DiskSpaceBar form={form} />
|
||||
|
||||
{isProjectNew ? (
|
||||
<FormMessage
|
||||
message="Disk size data is not available for ~10 minutes after project creation"
|
||||
type="error"
|
||||
/>
|
||||
) : (
|
||||
error && (
|
||||
<FormMessage message="Failed to load disk size data" type="error">
|
||||
{error?.message}
|
||||
</FormMessage>
|
||||
)
|
||||
)}
|
||||
|
||||
<DiskManagementDiskSizeReadReplicas
|
||||
isDirty={dirtyFields.totalSize !== undefined}
|
||||
totalSize={(defaultValues?.totalSize || 0) * 1.25}
|
||||
usedSize={mainDiskUsed}
|
||||
newTotalSize={watchedTotalSize * 1.25}
|
||||
oldStorageType={defaultValues?.storageType as DiskType}
|
||||
newStorageType={getValues('storageType') as DiskType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
min={includedDiskGB}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>GB</InputGroupText>
|
||||
{isDirty ? (
|
||||
<InputGroupButton
|
||||
type="button"
|
||||
variant="default"
|
||||
size="tiny"
|
||||
className="px-2 text-foreground-light"
|
||||
onClick={() => {
|
||||
resetField('totalSize')
|
||||
trigger('provisionedIOPS')
|
||||
}}
|
||||
title="Reset"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
||||
</InputGroupButton>
|
||||
) : null}
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -13,10 +13,8 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { DiskStorageSchemaType } from '../DiskManagement.schema'
|
||||
import {
|
||||
calculateComputeSizeRequiredForIops,
|
||||
calculateIOPSPrice,
|
||||
mapAddOnVariantIdToComputeSize,
|
||||
} from '../DiskManagement.utils'
|
||||
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
|
||||
import { ComputeSizeRecommendationSection } from '../ui/ComputeSizeRecommendationSection'
|
||||
import { DiskType, RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 } from '../ui/DiskManagement.constants'
|
||||
import { DiskManagementIOPSReadReplicas } from '../ui/DiskManagementReadReplicas'
|
||||
@@ -37,13 +35,6 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
|
||||
|
||||
const { isError } = useDiskAttributesQuery({ projectRef })
|
||||
|
||||
const iopsPrice = calculateIOPSPrice({
|
||||
oldStorageType: formState.defaultValues?.storageType as DiskType,
|
||||
oldProvisionedIOPS: formState.defaultValues?.provisionedIOPS || 0,
|
||||
newStorageType: getValues('storageType') as DiskType,
|
||||
newProvisionedIOPS: getValues('provisionedIOPS'),
|
||||
})
|
||||
|
||||
const disableIopsInput =
|
||||
RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(watchedComputeSize) && watchedStorageType === 'gp3'
|
||||
|
||||
@@ -55,7 +46,7 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
|
||||
const reccomendedComputeSize = calculateComputeSizeRequiredForIops(watchedIOPS)
|
||||
return (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
layout="flex-row-reverse"
|
||||
label="IOPS"
|
||||
id={field.name}
|
||||
description={
|
||||
@@ -87,21 +78,7 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
|
||||
</span>
|
||||
}
|
||||
labelOptional={
|
||||
<>
|
||||
<BillingChangeBadge
|
||||
show={
|
||||
(watchedStorageType !== formState.defaultValues?.storageType ||
|
||||
(watchedStorageType === 'gp3' &&
|
||||
field.value !== formState.defaultValues?.provisionedIOPS)) &&
|
||||
!formState.errors.provisionedIOPS &&
|
||||
!disableIopsInput
|
||||
}
|
||||
beforePrice={Number(iopsPrice.oldPrice)}
|
||||
afterPrice={Number(iopsPrice.newPrice)}
|
||||
className="mb-2"
|
||||
/>
|
||||
<p className="text-foreground-lighter">Input/output operations per second.</p>
|
||||
</>
|
||||
<p className="text-foreground-lighter">Input/output operations per second.</p>
|
||||
}
|
||||
>
|
||||
<FormControl className="max-w-32">
|
||||
|
||||
@@ -45,7 +45,7 @@ export function StorageTypeField({ form, disableInput }: StorageTypeFieldProps)
|
||||
name="storageType"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormItemLayout layout="horizontal" label="Storage type">
|
||||
<FormItemLayout layout="flex-row-reverse" label="Storage type" id={field.name}>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={async (e: DiskType) => {
|
||||
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
import { DiskStorageSchemaType } from '../DiskManagement.schema'
|
||||
import { calculateThroughputPrice } from '../DiskManagement.utils'
|
||||
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
|
||||
import {
|
||||
DISK_LIMITS,
|
||||
DiskType,
|
||||
@@ -40,12 +38,6 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {
|
||||
|
||||
useDiskAttributesQuery({ projectRef })
|
||||
|
||||
const throughputPrice = calculateThroughputPrice({
|
||||
storageType: form.getValues('storageType') as DiskType,
|
||||
newThroughput: form.getValues('throughput') || 0,
|
||||
oldThroughput: form.formState.defaultValues?.throughput || 0,
|
||||
})
|
||||
|
||||
const disableIopsInput =
|
||||
RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(watchedComputeSize) && watchedStorageType === 'gp3'
|
||||
|
||||
@@ -84,7 +76,7 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Throughput"
|
||||
layout="horizontal"
|
||||
layout="flex-row-reverse"
|
||||
description={
|
||||
<span className="flex flex-col gap-y-2">
|
||||
<p>Higher throughput suits applications with high data transfer needs.</p>
|
||||
@@ -100,21 +92,7 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {
|
||||
</span>
|
||||
}
|
||||
labelOptional={
|
||||
<>
|
||||
<BillingChangeBadge
|
||||
show={
|
||||
formState.isDirty &&
|
||||
formState.dirtyFields.throughput &&
|
||||
!formState.errors.throughput
|
||||
}
|
||||
beforePrice={Number(throughputPrice.oldPrice)}
|
||||
afterPrice={Number(throughputPrice.newPrice)}
|
||||
className="mb-2"
|
||||
/>
|
||||
<p className="text-foreground-lighter">
|
||||
Amount of data read/written per second.
|
||||
</p>
|
||||
</>
|
||||
<p className="text-foreground-lighter">Amount of data read/written per second.</p>
|
||||
}
|
||||
>
|
||||
<FormControl className="max-w-32">
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { type ComponentProps, type ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
|
||||
/**
|
||||
* @deprecated Use Admonition from ui-patterns instead
|
||||
* Pass actions as a prop to the Admonition component
|
||||
*/
|
||||
|
||||
interface NoticeBarProps extends Omit<ComponentProps<typeof Admonition>, 'description'> {
|
||||
title?: string
|
||||
description?: string
|
||||
icon?: ReactNode
|
||||
visible: boolean
|
||||
actions?: ReactNode
|
||||
}
|
||||
export function NoticeBar({ visible, description, actions, ...props }: NoticeBarProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0, y: 4 }}
|
||||
animate={{ opacity: 1, height: 'auto', y: 0 }}
|
||||
exit={{ opacity: 0, height: 0, y: 4 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<Admonition {...props} className={cn(props.className, 'mb-0')}>
|
||||
{description}
|
||||
<div className="flex flex-col gap-2">
|
||||
{actions && <div className="mt-2">{actions}</div>}
|
||||
</div>
|
||||
</Admonition>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
@@ -175,7 +175,7 @@ export const DiskUsage = ({
|
||||
{project.name}
|
||||
</span>
|
||||
<Button asChild variant="default" size={'tiny'}>
|
||||
<Link href={`/project/${project.ref}/settings/compute-and-disk`}>
|
||||
<Link href={`/project/${project.ref}/settings/infrastructure`}>
|
||||
Manage Disk
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
+29
-27
@@ -1,6 +1,8 @@
|
||||
import { useParams } from 'common'
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CardContent,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
@@ -11,21 +13,22 @@ import {
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionContent,
|
||||
PageSectionDescription,
|
||||
PageSectionMeta,
|
||||
PageSectionSummary,
|
||||
PageSectionTitle,
|
||||
} from 'ui-patterns/PageSection'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { ProjectUpgradeAlert } from '../General/Infrastructure/ProjectUpgradeAlert'
|
||||
import { ProjectUpgradeAlert } from '../../General/Infrastructure/ProjectUpgradeAlert'
|
||||
import {
|
||||
ReadReplicasWarning,
|
||||
ValidationErrorsWarning,
|
||||
ValidationWarningsAdmonition,
|
||||
} from './UpgradeWarnings'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDivider,
|
||||
ScaffoldSection,
|
||||
ScaffoldSectionContent,
|
||||
ScaffoldSectionDetail,
|
||||
} from '@/components/layouts/Scaffold'
|
||||
} from '../../Infrastructure/UpgradeWarnings'
|
||||
import AlertError from '@/components/ui/AlertError'
|
||||
import { useProjectUpgradeEligibilityQuery } from '@/data/config/project-upgrade-eligibility-query'
|
||||
import { useProjectServiceVersionsQuery } from '@/data/projects/project-service-versions'
|
||||
@@ -33,7 +36,7 @@ import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
|
||||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
import { useIsOrioleDb, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
|
||||
export const InfrastructureInfo = () => {
|
||||
export const ServiceVersionsSection = () => {
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
@@ -82,18 +85,18 @@ export const InfrastructureInfo = () => {
|
||||
const hasReadReplicas = (databases ?? []).length > 1
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldDivider />
|
||||
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<ScaffoldSectionDetail>
|
||||
<h4 className="text-base capitalize m-0">Service versions</h4>
|
||||
<p className="text-foreground-light text-sm pr-8 mt-1">
|
||||
Service versions and upgrade eligibility for your provisioned instance.
|
||||
</p>
|
||||
</ScaffoldSectionDetail>
|
||||
<ScaffoldSectionContent>
|
||||
<PageSection id="service-versions">
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Service versions</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
Service versions and upgrade eligibility for your provisioned instance.
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
{isInactive ? (
|
||||
<Admonition
|
||||
type="note"
|
||||
@@ -103,7 +106,6 @@ export const InfrastructureInfo = () => {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* [Joshen] Double check why we need this waterfall loading behaviour here */}
|
||||
{isLoadingUpgradeEligibility && <GenericSkeletonLoader />}
|
||||
{isErrorUpgradeEligibility && (
|
||||
<AlertError error={error} subject="Failed to retrieve Postgres version" />
|
||||
@@ -214,9 +216,9 @@ export const InfrastructureInfo = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
</>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
export interface Attribute {
|
||||
key: string
|
||||
name?: string
|
||||
color: 'white' | 'blue' | 'green' | 'yellow' | 'orange'
|
||||
}
|
||||
export interface CategoryAttribute {
|
||||
anchor: string
|
||||
key: string // Property from organization usage
|
||||
attributes: Attribute[] // For querying against stats-daily / infra-monitoring
|
||||
name: string
|
||||
unit: 'bytes' | 'absolute' | 'percentage'
|
||||
links: {
|
||||
name: string
|
||||
url: string
|
||||
}[]
|
||||
description: string
|
||||
chartPrefix?: 'Max' | 'Average'
|
||||
chartDescription: string
|
||||
}
|
||||
|
||||
export interface CategoryMeta {
|
||||
key: string
|
||||
name: string
|
||||
description: string
|
||||
attributes: CategoryAttribute[]
|
||||
}
|
||||
|
||||
export const INFRA_ACTIVITY_METRICS: CategoryMeta[] = [
|
||||
{
|
||||
key: 'infra',
|
||||
name: 'Infrastructure',
|
||||
description: 'Usage statistics related to your server instance',
|
||||
attributes: [
|
||||
{
|
||||
anchor: 'cpu',
|
||||
key: 'max_cpu_usage',
|
||||
attributes: [{ key: 'max_cpu_usage', color: 'white' }],
|
||||
name: 'CPU',
|
||||
unit: 'percentage',
|
||||
description: 'Max CPU usage of your server.',
|
||||
chartDescription: '',
|
||||
links: [
|
||||
{
|
||||
name: 'Compute Add-Ons',
|
||||
url: `${DOCS_URL}/guides/platform/compute-add-ons`,
|
||||
},
|
||||
{
|
||||
name: 'High CPU Usage',
|
||||
url: `${DOCS_URL}/guides/troubleshooting/high-cpu-usage`,
|
||||
},
|
||||
{
|
||||
name: 'Metrics',
|
||||
url: `${DOCS_URL}/guides/platform/metrics`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
anchor: 'ram',
|
||||
key: 'ram_usage',
|
||||
attributes: [{ key: 'ram_usage', color: 'white' }],
|
||||
name: 'Memory',
|
||||
unit: 'percentage',
|
||||
description:
|
||||
'Memory usage of your server.\nYou might observe elevated memory usage, even with little to no load. Besides Postgres, a wide range of services are running under the hood resulting in an elevated base memory usage.',
|
||||
chartDescription: '',
|
||||
links: [
|
||||
{
|
||||
name: 'Compute Add-Ons',
|
||||
url: `${DOCS_URL}/guides/platform/compute-add-ons`,
|
||||
},
|
||||
{
|
||||
name: 'High RAM Usage',
|
||||
url: `${DOCS_URL}/guides/troubleshooting/exhaust-ram`,
|
||||
},
|
||||
{
|
||||
name: 'Metrics',
|
||||
url: `${DOCS_URL}/guides/platform/metrics`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
anchor: 'disk_io',
|
||||
key: 'disk_io_consumption',
|
||||
attributes: [{ key: 'disk_io_consumption', color: 'white' }],
|
||||
name: 'Disk IO Bandwidth',
|
||||
unit: 'percentage',
|
||||
links: [
|
||||
{
|
||||
name: 'Disk Throughput and IOPS',
|
||||
url: `${DOCS_URL}/guides/platform/compute-add-ons#disk-throughput-and-iops`,
|
||||
},
|
||||
{
|
||||
name: 'High Disk I/O',
|
||||
url: `${DOCS_URL}/guides/troubleshooting/exhaust-disk-io`,
|
||||
},
|
||||
{
|
||||
name: 'Metrics',
|
||||
url: `${DOCS_URL}/guides/platform/metrics`,
|
||||
},
|
||||
],
|
||||
description:
|
||||
'The disk performance of your workload is determined by the Disk IO bandwidth.\nSmaller compute instances (below 4XL) can burst above their baseline disk throughput and IOPS for short periods of time. Beyond that, the performance reverts to the baseline.',
|
||||
chartDescription: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1,457 +0,0 @@
|
||||
import { useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { capitalize } from 'lodash'
|
||||
import { BarChart2, ChartLine, ExternalLink } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants'
|
||||
import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { CPUWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/CPUWarnings'
|
||||
import { DiskIOBandwidthWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings'
|
||||
import { RAMWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/RAMWarnings'
|
||||
import UsageBarChart from '@/components/interfaces/Organization/Usage/UsageBarChart'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDivider,
|
||||
ScaffoldSection,
|
||||
ScaffoldSectionContent,
|
||||
ScaffoldSectionDetail,
|
||||
} from '@/components/layouts/Scaffold'
|
||||
import { DatabaseSelector } from '@/components/ui/DatabaseSelector'
|
||||
import { DateRangePicker } from '@/components/ui/DateRangePicker'
|
||||
import { DocsButton } from '@/components/ui/DocsButton'
|
||||
import Panel from '@/components/ui/Panel'
|
||||
import { DataPoint } from '@/data/analytics/constants'
|
||||
import { mapMultiResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries'
|
||||
import {
|
||||
InfraMonitoringAttribute,
|
||||
useInfraMonitoringAttributesQuery,
|
||||
} from '@/data/analytics/infra-monitoring-query'
|
||||
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
|
||||
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
|
||||
import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query'
|
||||
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { DOCS_URL, INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS, InstanceSpecs } from '@/lib/constants'
|
||||
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics'
|
||||
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
|
||||
|
||||
const NON_DEDICATED_IO_RESOURCES = [
|
||||
'ci_micro',
|
||||
'ci_small',
|
||||
'ci_medium',
|
||||
'ci_large',
|
||||
'ci_xlarge',
|
||||
'ci_2xlarge',
|
||||
]
|
||||
|
||||
const INFRA_ATTRIBUTES: InfraMonitoringAttribute[] = [
|
||||
'max_cpu_usage',
|
||||
'ram_usage',
|
||||
'disk_io_consumption',
|
||||
]
|
||||
|
||||
export const InfrastructureActivity = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
const [dateRange, setDateRange] = useState<any>()
|
||||
|
||||
const { data: subscription, isPending: isLoadingSubscription } = useOrgSubscriptionQuery({
|
||||
orgSlug: organization?.slug,
|
||||
})
|
||||
const { hasAccess: hasAccessToComputeSizes } = useCheckEntitlements(
|
||||
'instances.compute_update_available_sizes'
|
||||
)
|
||||
|
||||
const { data: resourceWarnings } = useResourceWarningsQuery({ ref: projectRef })
|
||||
// [Joshen Cleanup] JFYI this client side filtering can be cleaned up once BE changes are live which will only return the warnings based on the provided ref
|
||||
const projectResourceWarnings = resourceWarnings?.find((x) => x.project === projectRef)
|
||||
|
||||
const { data: addons } = useProjectAddonsQuery({ projectRef })
|
||||
const selectedAddons = addons?.selected_addons ?? []
|
||||
|
||||
const { computeInstance } = getAddons(selectedAddons)
|
||||
const hasDedicatedIOResources =
|
||||
computeInstance !== undefined &&
|
||||
!NON_DEDICATED_IO_RESOURCES.includes(computeInstance.variant.identifier)
|
||||
|
||||
function getCurrentComputeInstanceSpecs() {
|
||||
if (computeInstance?.variant.meta) {
|
||||
// If user has a compute instance (called addons) return that
|
||||
return computeInstance?.variant.meta as InstanceSpecs
|
||||
} else {
|
||||
// Otherwise, return the default specs
|
||||
return project?.infra_compute_size === 'nano' ? INSTANCE_NANO_SPECS : INSTANCE_MICRO_SPECS
|
||||
}
|
||||
}
|
||||
|
||||
const currentComputeInstanceSpecs = getCurrentComputeInstanceSpecs()
|
||||
|
||||
const currentBillingCycleSelected = useMemo(() => {
|
||||
// Selected by default
|
||||
if (!dateRange?.period_start || !dateRange?.period_end || !subscription) return true
|
||||
|
||||
const { current_period_start, current_period_end } = subscription
|
||||
|
||||
return (
|
||||
dayjs(dateRange.period_start.date).isSame(new Date(current_period_start * 1000)) &&
|
||||
dayjs(dateRange.period_end.date).isSame(new Date(current_period_end * 1000))
|
||||
)
|
||||
}, [dateRange, subscription])
|
||||
|
||||
const upgradeUrl =
|
||||
organization === undefined
|
||||
? `/`
|
||||
: hasAccessToComputeSizes
|
||||
? `/org/${organization?.slug ?? '[slug]'}/billing#subscription`
|
||||
: `/project/${projectRef}/settings/addons`
|
||||
|
||||
const categoryMeta = INFRA_ACTIVITY_METRICS.find((category) => category.key === 'infra')
|
||||
|
||||
const startDate = useMemo(() => {
|
||||
if (dateRange?.period_start?.date === 'Invalid Date') return undefined
|
||||
|
||||
// If end date is in future, set end date to now
|
||||
if (!dateRange?.period_start?.date) {
|
||||
return undefined
|
||||
} else {
|
||||
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
||||
return new Date(dateRange?.period_start?.date ?? 0).toISOString().slice(0, -5) + 'Z'
|
||||
}
|
||||
}, [dateRange])
|
||||
|
||||
const endDate = useMemo(() => {
|
||||
if (dateRange?.period_end?.date === 'Invalid Date') return undefined
|
||||
|
||||
// If end date is in future, set end date to end of current day
|
||||
if (dateRange?.period_end?.date && dayjs(dateRange.period_end.date).isAfter(dayjs())) {
|
||||
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
||||
// In order to have full days from Prometheus metrics when using 1d interval,
|
||||
// the time needs to be greater or equal than the time of the start date
|
||||
return dayjs().endOf('day').toISOString().slice(0, -5) + 'Z'
|
||||
} else if (dateRange?.period_end?.date) {
|
||||
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
||||
return new Date(dateRange.period_end.date ?? 0).toISOString().slice(0, -5) + 'Z'
|
||||
}
|
||||
}, [dateRange])
|
||||
|
||||
// Switch to hourly interval, if the timeframe is <48 hours
|
||||
let interval: '1d' | '1h' = '1d'
|
||||
let dateFormat = 'DD MMM'
|
||||
if (startDate && endDate) {
|
||||
const diffInHours = dayjs(endDate).diff(startDate, 'hours')
|
||||
|
||||
if (diffInHours <= 48) {
|
||||
interval = '1h'
|
||||
dateFormat = 'h a'
|
||||
}
|
||||
}
|
||||
|
||||
const { data: infraMonitoringData, isPending: isLoadingInfraData } =
|
||||
useInfraMonitoringAttributesQuery({
|
||||
projectRef,
|
||||
attributes: INFRA_ATTRIBUTES,
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
databaseIdentifier: state.selectedDatabaseId,
|
||||
})
|
||||
|
||||
const transformedData = useMemo(() => {
|
||||
if (!infraMonitoringData) return undefined
|
||||
return mapMultiResponseToAnalyticsData(infraMonitoringData, INFRA_ATTRIBUTES, dateFormat)
|
||||
}, [infraMonitoringData, dateFormat])
|
||||
|
||||
const cpuUsageData = transformedData?.max_cpu_usage
|
||||
const memoryUsageData = transformedData?.ram_usage
|
||||
const ioBudgetData = transformedData?.disk_io_consumption
|
||||
|
||||
const hasLatest = dayjs(endDate!).isAfter(dayjs().startOf('day'))
|
||||
|
||||
const latestIoBudgetConsumption =
|
||||
hasLatest && ioBudgetData?.data?.slice(-1)?.[0]
|
||||
? Number(ioBudgetData.data.slice(-1)[0].disk_io_consumption)
|
||||
: 0
|
||||
|
||||
const highestIoBudgetConsumption = (ioBudgetData?.data || [])
|
||||
.map((x) => Number(x.disk_io_consumption) ?? 0)
|
||||
.reduce((a, b) => Math.max(a, b), 0)
|
||||
|
||||
const chartMeta: { [key: string]: { data: DataPoint[]; isLoading: boolean } } = {
|
||||
max_cpu_usage: {
|
||||
isLoading: isLoadingInfraData,
|
||||
data: cpuUsageData?.data ?? [],
|
||||
},
|
||||
ram_usage: {
|
||||
isLoading: isLoadingInfraData,
|
||||
data: memoryUsageData?.data ?? [],
|
||||
},
|
||||
disk_io_consumption: {
|
||||
isLoading: isLoadingInfraData,
|
||||
data: ioBudgetData?.data ?? [],
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer id="infrastructure-activity">
|
||||
<div className="mx-auto flex flex-col gap-10 pt-6">
|
||||
<div>
|
||||
<p className="text-xl">Infrastructure Activity</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Activity statistics related to your server instance
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldContainer className="sticky top-0 py-6 border-b bg-studio z-10">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<DatabaseSelector />
|
||||
{!isLoadingSubscription && (
|
||||
<>
|
||||
<DateRangePicker
|
||||
onChange={setDateRange}
|
||||
value={TIME_PERIODS_REPORTS[0].key}
|
||||
options={[...TIME_PERIODS_BILLING, ...TIME_PERIODS_REPORTS]}
|
||||
loading={isLoadingSubscription}
|
||||
currentBillingPeriodStart={subscription?.current_period_start}
|
||||
currentBillingPeriodEnd={subscription?.current_period_end}
|
||||
/>
|
||||
<p className="text-sm text-foreground-light">
|
||||
{dayjs(startDate).format('DD MMM YYYY')} - {dayjs(endDate).format('DD MMM YYYY')}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ScaffoldContainer>
|
||||
|
||||
{categoryMeta?.attributes.map((attribute) => {
|
||||
const chartData = chartMeta[attribute.key]?.data ?? []
|
||||
|
||||
return (
|
||||
<Fragment key={attribute.key}>
|
||||
<ScaffoldContainer id={attribute.anchor}>
|
||||
<ScaffoldSection>
|
||||
<ScaffoldSectionDetail>
|
||||
<div className="sticky top-32 space-y-6">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="text-base capitalize m-0">{attribute.name}</h4>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
{attribute.description.split('\n').map((value, idx) => (
|
||||
<p key={`desc-${idx}`} className="text-sm text-foreground-light pr-8 m-0">
|
||||
{value}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-foreground mb-2">More information</p>
|
||||
{attribute.links.map((link) => (
|
||||
<div key={link.url}>
|
||||
<Link href={link.url} target="_blank" rel="noreferrer">
|
||||
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
|
||||
<p className="text-sm m-0">{link.name}</p>
|
||||
<ExternalLink size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScaffoldSectionDetail>
|
||||
<ScaffoldSectionContent>
|
||||
{attribute.key === 'disk_io_consumption' && (
|
||||
<>
|
||||
<DiskIOBandwidthWarnings
|
||||
upgradeUrl={upgradeUrl}
|
||||
hasAccessToComputeSizes={hasAccessToComputeSizes}
|
||||
hasLatest={hasLatest}
|
||||
currentBillingCycleSelected={currentBillingCycleSelected}
|
||||
latestIoBudgetConsumption={latestIoBudgetConsumption}
|
||||
highestIoBudgetConsumption={highestIoBudgetConsumption}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<p>Disk IO Bandwidth</p>
|
||||
|
||||
{currentComputeInstanceSpecs.baseline_disk_io_mbs ===
|
||||
currentComputeInstanceSpecs.max_disk_io_mbs ? (
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your current compute has a baseline and maximum disk throughput of{' '}
|
||||
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your current compute can burst above the baseline disk throughput of{' '}
|
||||
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
|
||||
Mbps for short periods of time.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm mb-2">Overview</p>
|
||||
<div className="flex items-center justify-between border-b py-1">
|
||||
<p className="text-xs text-foreground-light">Current compute instance</p>
|
||||
<p className="text-xs">
|
||||
{computeInstance?.variant?.name ??
|
||||
capitalize(project?.infra_compute_size) ??
|
||||
'Micro'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b py-1">
|
||||
<p className="text-xs text-foreground-light">Baseline IO Bandwidth</p>
|
||||
<p className="text-xs">
|
||||
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
|
||||
Mbps
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b py-1">
|
||||
<p className="text-xs text-foreground-light">
|
||||
Maximum IO Bandwidth (burst limit)
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps
|
||||
</p>
|
||||
</div>
|
||||
{currentComputeInstanceSpecs.max_disk_io_mbs !==
|
||||
currentComputeInstanceSpecs?.baseline_disk_io_mbs && (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<p className="text-xs text-foreground-light">Daily burst time limit</p>
|
||||
<p className="text-xs">30 mins</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{attribute.key === 'max_cpu_usage' && (
|
||||
<CPUWarnings
|
||||
upgradeUrl={upgradeUrl}
|
||||
hasAccessToComputeSizes={hasAccessToComputeSizes}
|
||||
severity={projectResourceWarnings?.cpu_exhaustion}
|
||||
/>
|
||||
)}
|
||||
{attribute.key === 'ram_usage' && (
|
||||
<RAMWarnings
|
||||
upgradeUrl={upgradeUrl}
|
||||
hasAccessToComputeSizes={hasAccessToComputeSizes}
|
||||
severity={projectResourceWarnings?.memory_and_swap_exhaustion}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-row justify-between">
|
||||
{attribute.key === 'disk_io_consumption' ? (
|
||||
<p>Disk IO consumed per {interval === '1d' ? 'day' : 'hour'}</p>
|
||||
) : (
|
||||
<p>
|
||||
Max{' '}
|
||||
<span className={attribute.key === 'ram_usage' ? 'lowercase' : ''}>
|
||||
{attribute.name}
|
||||
</span>{' '}
|
||||
utilization per {interval === '1d' ? 'day' : 'hour'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{attribute.key === 'ram_usage' && (
|
||||
<div className="text-sm text-foreground-light">
|
||||
<p>
|
||||
Your compute instance has {currentComputeInstanceSpecs.memory_gb} GB of
|
||||
memory.
|
||||
</p>
|
||||
{currentComputeInstanceSpecs.memory_gb === 1 && (
|
||||
<p>
|
||||
As your project is running on the smallest compute instance, it is not
|
||||
unusual for your project to have a base memory usage of ~50%.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attribute.key === 'max_cpu_usage' && (
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your compute instance has {currentComputeInstanceSpecs.cpu_cores} CPU cores.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{attribute.chartDescription.split('\n').map((paragraph, idx) => (
|
||||
<p key={`para-${idx}`} className="text-sm text-foreground-light">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{attribute.key === 'disk_io_consumption' && hasDedicatedIOResources ? (
|
||||
<>
|
||||
<Admonition
|
||||
type="note"
|
||||
title={`Your compute instance of ${computeInstance.variant.name} comes with dedicated I/O resources`}
|
||||
description="Your project thus does not rely on I/O balance or burst capacity as larger
|
||||
add-ons are designed for sustained, high performance with specific IOPS and
|
||||
throughput limits without needing to burst."
|
||||
>
|
||||
<DocsButton
|
||||
abbrev={false}
|
||||
href={`${DOCS_URL}/guides/platform/compute-add-ons#disk-throughput-and-iops`}
|
||||
/>
|
||||
</Admonition>
|
||||
</>
|
||||
) : chartMeta[attribute.key].isLoading ? (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
) : chartData.length ? (
|
||||
<UsageBarChart
|
||||
name={`${attribute.chartPrefix || ''} ${attribute.name}`}
|
||||
unit={attribute.unit}
|
||||
attributes={attribute.attributes}
|
||||
data={chartData}
|
||||
yFormatter={(value) => `${Math.round(Number(value))}%`}
|
||||
tooltipFormatter={(value) => `${value}%`}
|
||||
yLimit={100}
|
||||
/>
|
||||
) : (
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="flex flex-col items-center justify-center space-y-2">
|
||||
<BarChart2 size={18} className="text-foreground-light mb-2" />
|
||||
<p className="text-sm">No data in period</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
May take a few minutes to show
|
||||
</p>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
)}
|
||||
{attribute.key === 'disk_io_consumption' && !hasDedicatedIOResources && (
|
||||
<Admonition
|
||||
type="default"
|
||||
title="Looking for actual disk activity?"
|
||||
description="The chart above shows your remaining burst budget, not real disk throughput. For detailed read/write IOPS and throughput charts, head to the Database Observability page."
|
||||
>
|
||||
<Button asChild variant="default" icon={<ChartLine size={14} />}>
|
||||
<Link href={`/project/${projectRef}/observability/database`}>
|
||||
View detailed IOPS and throughput
|
||||
</Link>
|
||||
</Button>
|
||||
</Admonition>
|
||||
)}
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldDivider />
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -301,7 +301,7 @@ const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationU
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52 *:space-x-2">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/project/${projectRef}/settings/compute-and-disk`}>
|
||||
<Link href={`/project/${projectRef}/settings/infrastructure`}>
|
||||
Resize databases
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
+2
-2
@@ -139,11 +139,11 @@ const StorageListV2MigrationDialog = () => {
|
||||
|
||||
<p className="text-sm">
|
||||
Ensure that your database instance has not{' '}
|
||||
<InlineLink href={`/project/${ref}/settings/compute-and-disk#disk-size`}>
|
||||
<InlineLink href={`/project/${ref}/settings/infrastructure#disk-size`}>
|
||||
scaled disk
|
||||
</InlineLink>{' '}
|
||||
within the last 6h and you have at least 60%{' '}
|
||||
<InlineLink href={`/project/${ref}/settings/infrastructure#infrastructure-activity`}>
|
||||
<InlineLink href={`/project/${ref}/observability/database`}>
|
||||
CPU capacity
|
||||
</InlineLink>{' '}
|
||||
before proceeding.
|
||||
|
||||
@@ -189,10 +189,8 @@ const IssueSuggestion = ({ category, projectRef }: { category: string; projectRe
|
||||
title="Have you checked your project's infrastructure activity?"
|
||||
>
|
||||
High memory or low disk IO bandwidth may be slowing down your database. Verify by checking
|
||||
the infrastructure activity of your project{' '}
|
||||
<InlineLink href={`${baseUrl}/settings/infrastructure#infrastructure-activity`}>
|
||||
here
|
||||
</InlineLink>
|
||||
your project's database observability reports{' '}
|
||||
<InlineLink href={`${baseUrl}/observability/database`}>here</InlineLink>
|
||||
.
|
||||
</Admonition>
|
||||
)
|
||||
@@ -205,11 +203,8 @@ const IssueSuggestion = ({ category, projectRef }: { category: string; projectRe
|
||||
className={className}
|
||||
title="Have you checked the Query Performance Advisor?"
|
||||
>
|
||||
Identify slow running queries and get actionable insights on how to optimize them with the
|
||||
Query Performance Advisor{' '}
|
||||
<InlineLink href={`${baseUrl}/settings/infrastructure#infrastructure-activity`}>
|
||||
here
|
||||
</InlineLink>
|
||||
Identify slow running queries and get actionable insights on how to optimize them with Query
|
||||
Performance <InlineLink href={`${baseUrl}/observability/query-performance`}>here</InlineLink>
|
||||
.
|
||||
</Admonition>
|
||||
)
|
||||
|
||||
@@ -106,7 +106,6 @@ describe('useGenerateSettingsMenu (self-hosted)', () => {
|
||||
const { result } = renderHook(() => useGenerateSettingsMenu())
|
||||
const configGroup = result.current.find((group) => group.title === 'Configuration')
|
||||
|
||||
expect(configGroup?.items.some((item) => item.key === 'compute-and-disk')).toBe(false)
|
||||
expect(configGroup?.items.some((item) => item.key === 'infrastructure')).toBe(false)
|
||||
expect(configGroup?.items.some((item) => item.key === 'addons')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -147,9 +147,6 @@ describe('useGenerateSettingsMenu', () => {
|
||||
)
|
||||
|
||||
expect(shortcutByKey.get('general')).toBe(SHORTCUT_IDS.NAV_PROJECT_SETTINGS_GENERAL)
|
||||
expect(shortcutByKey.get('compute-and-disk')).toBe(
|
||||
SHORTCUT_IDS.NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK
|
||||
)
|
||||
expect(shortcutByKey.get('infrastructure')).toBe(
|
||||
SHORTCUT_IDS.NAV_PROJECT_SETTINGS_INFRASTRUCTURE
|
||||
)
|
||||
|
||||
@@ -106,15 +106,6 @@ export const useGenerateSettingsMenu = () => {
|
||||
items: [],
|
||||
shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_GENERAL,
|
||||
},
|
||||
{
|
||||
name: 'Compute and Disk',
|
||||
key: 'compute-and-disk',
|
||||
url: `/project/${ref}/settings/compute-and-disk`,
|
||||
items: [],
|
||||
disabled: !isProjectActive,
|
||||
isLoading: isPending,
|
||||
shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK,
|
||||
},
|
||||
{
|
||||
name: 'Infrastructure',
|
||||
key: 'infrastructure',
|
||||
|
||||
@@ -58,7 +58,7 @@ export const BannerFreeMicroUpgrade = () => {
|
||||
track('free_micro_upgrade_banner_cta_clicked')
|
||||
}}
|
||||
>
|
||||
<Link href={`/project/${ref}/settings/compute-and-disk?upgrade=micro`}>
|
||||
<Link href={`/project/${ref}/settings/infrastructure?upgrade=micro`}>
|
||||
Upgrade for free
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -201,7 +201,7 @@ export const ComputeBadgeWrapper = ({
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Link href={`/project/${projectRef}/settings/compute-and-disk`}>
|
||||
<Link href={`/project/${projectRef}/settings/infrastructure`}>
|
||||
Upgrade compute
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
+9
-9
@@ -115,11 +115,11 @@ export const ResourceExhaustionWarningBanner = () => {
|
||||
const correctionUrlVariants = {
|
||||
undefined: undefined,
|
||||
null: '/project/[ref]/settings/[infra-path]',
|
||||
disk_space: '/project/[ref]/settings/compute-and-disk',
|
||||
read_only: '/project/[ref]/settings/compute-and-disk',
|
||||
disk_io: '/project/[ref]/settings/compute-and-disk',
|
||||
cpu: '/project/[ref]/settings/compute-and-disk',
|
||||
ram: '/project/[ref]/settings/compute-and-disk',
|
||||
disk_space: '/project/[ref]/settings/infrastructure',
|
||||
read_only: '/project/[ref]/settings/infrastructure',
|
||||
disk_io: '/project/[ref]/settings/infrastructure',
|
||||
cpu: '/project/[ref]/settings/infrastructure',
|
||||
ram: '/project/[ref]/settings/infrastructure',
|
||||
auth_email_rate_limit: '/project/[ref]/auth/rate-limits',
|
||||
auth_restricted_email_sending: '/project/[ref]/auth/smtp',
|
||||
default: (metric: string) => `/project/[ref]/settings/[infra-path]#${metric}`,
|
||||
@@ -146,7 +146,7 @@ export const ResourceExhaustionWarningBanner = () => {
|
||||
return `/org/${organization?.slug ?? '_'}/billing?panel=subscriptionPlan&source=resource_exhaustion_banner`
|
||||
}
|
||||
if (isComputeUpgradeMetric && activeWarnings.length > 1) {
|
||||
return `/project/${ref ?? 'default'}/settings/compute-and-disk`
|
||||
return `/project/${ref ?? 'default'}/settings/infrastructure`
|
||||
}
|
||||
return getCorrectionUrl(metric)
|
||||
?.replace('[ref]', ref ?? 'default')
|
||||
@@ -182,15 +182,15 @@ export const ResourceExhaustionWarningBanner = () => {
|
||||
warningContent === undefined || (!warningContent?.title && !warningContent?.description)
|
||||
const isUsageOrInfraPage =
|
||||
router.pathname.endsWith('/usage') || router.pathname.endsWith('/infrastructure')
|
||||
// Compute warnings now link to compute-and-disk, so they should remain visible on infrastructure
|
||||
// Compute warnings now link to infrastructure, so they should remain visible on usage.
|
||||
const onUsageOrInfraAndNotInReadOnlyMode =
|
||||
isUsageOrInfraPage &&
|
||||
!activeWarnings.includes('is_readonly_mode_enabled') &&
|
||||
!isComputeUpgradeMetric
|
||||
// Suppress when already on the target page (no-op CTA). Paid-plan compute warnings link to
|
||||
// compute-and-disk; free-plan links to billing instead, so we keep the banner visible for them.
|
||||
// infrastructure; free-plan links to billing instead, so we keep the banner visible for them.
|
||||
const onDatabaseSettingsAndInReadOnlyMode =
|
||||
router.pathname.endsWith('settings/compute-and-disk') &&
|
||||
router.pathname.endsWith('settings/infrastructure') &&
|
||||
(activeWarnings.includes('is_readonly_mode_enabled') || (isComputeUpgradeMetric && !isFreePlan))
|
||||
|
||||
// these take precedence over each other, so there's only one active warning to check
|
||||
|
||||
@@ -71,7 +71,7 @@ export const UpgradePlanButton = ({
|
||||
? `/org/${slug ?? '_'}/billing?panel=costControl&source=${source}`
|
||||
: isOnPaidPlanAndRequestingToPurchaseAddon
|
||||
? addon === 'computeSize'
|
||||
? `/project/${ref ?? '_'}/settings/compute-and-disk`
|
||||
? `/project/${ref ?? '_'}/settings/infrastructure`
|
||||
: `/project/${ref ?? '_'}/settings/addons?panel=${addon}&source=${source}`
|
||||
: `/org/${slug ?? '_'}/billing?panel=subscriptionPlan&source=${source}`
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { DiskSizeConfiguration } from '@/components/interfaces/Settings/Database
|
||||
import { JitDbAccessConfiguration } from '@/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessConfiguration'
|
||||
import { NetworkRestrictions } from '@/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions'
|
||||
import { PoolingModesModal } from '@/components/interfaces/Settings/Database/PoolingModesModal'
|
||||
import { ServiceVersionsSection } from '@/components/interfaces/Settings/Database/ServiceVersions/ServiceVersionsSection'
|
||||
import { SettingsDatabaseEmptyStateLocal } from '@/components/interfaces/Settings/Database/SettingsDatabaseEmptyStateLocal'
|
||||
import { SSLConfiguration } from '@/components/interfaces/Settings/Database/SSLConfiguration'
|
||||
import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
|
||||
@@ -57,8 +58,9 @@ const DatabaseSettings: NextPageWithLayout = () => {
|
||||
{jitDbAccessEnabled && <JitDbAccessConfiguration />}
|
||||
<ConnectionPooling />
|
||||
<SSLConfiguration />
|
||||
<ServiceVersionsSection />
|
||||
{showNewDiskManagementUI ? (
|
||||
// This form is hidden if Disk and Compute form is enabled, new form is on ./settings/compute-and-disk
|
||||
// This form is hidden if Disk and Compute form is enabled, new form is on ./settings/infrastructure
|
||||
<DiskManagementPanelForm />
|
||||
) : (
|
||||
<DiskSizeConfiguration />
|
||||
|
||||
@@ -373,7 +373,7 @@ const DatabaseUsage = () => {
|
||||
<div className="ml-auto">
|
||||
{project?.cloud_provider === 'AWS' ? (
|
||||
<Button asChild variant="default">
|
||||
<Link href={`/project/${ref}/settings/compute-and-disk`}>
|
||||
<Link href={`/project/${ref}/settings/infrastructure`}>
|
||||
Increase disk size
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { DiskManagementForm } from '@/components/interfaces/DiskManagement/DiskManagementForm'
|
||||
import DefaultLayout from '@/components/layouts/DefaultLayout'
|
||||
import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDescription,
|
||||
ScaffoldHeader,
|
||||
ScaffoldTitle,
|
||||
} from '@/components/layouts/Scaffold'
|
||||
import type { NextPageWithLayout } from '@/types'
|
||||
|
||||
const AuthSettings: NextPageWithLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldHeader>
|
||||
<ScaffoldTitle>Compute and Disk</ScaffoldTitle>
|
||||
<ScaffoldDescription>
|
||||
Configure the compute and disk settings for your project.
|
||||
</ScaffoldDescription>
|
||||
</ScaffoldHeader>
|
||||
</ScaffoldContainer>
|
||||
<DiskManagementForm />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
AuthSettings.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<SettingsLayout title="Compute and Disk">{page}</SettingsLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
export default AuthSettings
|
||||
@@ -1,38 +1,37 @@
|
||||
import { InfrastructureActivity } from '@/components/interfaces/Settings/Infrastructure/InfrastructureActivity'
|
||||
import { InfrastructureInfo } from '@/components/interfaces/Settings/Infrastructure/InfrastructureInfo'
|
||||
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
|
||||
import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDescription,
|
||||
ScaffoldDivider,
|
||||
ScaffoldHeader,
|
||||
ScaffoldTitle,
|
||||
} from '@/components/layouts/Scaffold'
|
||||
PageHeader,
|
||||
PageHeaderDescription,
|
||||
PageHeaderMeta,
|
||||
PageHeaderSummary,
|
||||
PageHeaderTitle,
|
||||
} from 'ui-patterns/PageHeader'
|
||||
|
||||
import { DiskManagementForm } from '@/components/interfaces/DiskManagement/DiskManagementForm'
|
||||
import DefaultLayout from '@/components/layouts/DefaultLayout'
|
||||
import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout'
|
||||
import type { NextPageWithLayout } from '@/types'
|
||||
|
||||
const ProjectInfrastructure: NextPageWithLayout = () => {
|
||||
const InfrastructureSettings: NextPageWithLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldHeader>
|
||||
<ScaffoldTitle>Infrastructure</ScaffoldTitle>
|
||||
<ScaffoldDescription>
|
||||
General information regarding your server instance
|
||||
</ScaffoldDescription>
|
||||
</ScaffoldHeader>
|
||||
</ScaffoldContainer>
|
||||
<InfrastructureInfo />
|
||||
<ScaffoldDivider />
|
||||
<InfrastructureActivity />
|
||||
<PageHeader size="default" className="pb-12">
|
||||
<PageHeaderMeta>
|
||||
<PageHeaderSummary>
|
||||
<PageHeaderTitle>Infrastructure</PageHeaderTitle>
|
||||
<PageHeaderDescription>
|
||||
View and configure compute and disk for your project.
|
||||
</PageHeaderDescription>
|
||||
</PageHeaderSummary>
|
||||
</PageHeaderMeta>
|
||||
</PageHeader>
|
||||
<DiskManagementForm />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ProjectInfrastructure.getLayout = (page) => (
|
||||
InfrastructureSettings.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<SettingsLayout title="Infrastructure">{page}</SettingsLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default ProjectInfrastructure
|
||||
export default InfrastructureSettings
|
||||
@@ -109,6 +109,11 @@ export const SHARED_REDIRECTS: StudioRedirect[] = [
|
||||
destination: '/org/_/billing?panel=subscriptionPlan',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/project/:ref/settings/compute-and-disk',
|
||||
destination: '/project/:ref/settings/infrastructure',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/project/:ref/settings/billing/subscription',
|
||||
has: [{ type: 'query', key: 'panel', value: 'pitr' }],
|
||||
@@ -118,7 +123,7 @@ export const SHARED_REDIRECTS: StudioRedirect[] = [
|
||||
{
|
||||
source: '/project/:ref/settings/billing/subscription',
|
||||
has: [{ type: 'query', key: 'panel', value: 'computeInstance' }],
|
||||
destination: '/project/:ref/settings/compute-and-disk',
|
||||
destination: '/project/:ref/settings/infrastructure',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,7 +10,6 @@ import { RegistryDefinations } from '../types'
|
||||
*/
|
||||
export const PROJECT_SETTINGS_NAV_SHORTCUT_IDS = {
|
||||
NAV_PROJECT_SETTINGS_GENERAL: 'nav.project-settings-general',
|
||||
NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK: 'nav.project-settings-compute-and-disk',
|
||||
NAV_PROJECT_SETTINGS_INFRASTRUCTURE: 'nav.project-settings-infrastructure',
|
||||
NAV_PROJECT_SETTINGS_INTEGRATIONS: 'nav.project-settings-integrations',
|
||||
NAV_PROJECT_SETTINGS_WEBHOOKS: 'nav.project-settings-webhooks',
|
||||
@@ -32,13 +31,6 @@ export const projectSettingsNavRegistry: RegistryDefinations<ProjectSettingsNavS
|
||||
showInSettings: false,
|
||||
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_PROJECT_SETTINGS,
|
||||
},
|
||||
[PROJECT_SETTINGS_NAV_SHORTCUT_IDS.NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK]: {
|
||||
id: PROJECT_SETTINGS_NAV_SHORTCUT_IDS.NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK,
|
||||
label: 'Go to Compute and Disk',
|
||||
sequence: ['S', 'C'],
|
||||
showInSettings: false,
|
||||
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_PROJECT_SETTINGS,
|
||||
},
|
||||
[PROJECT_SETTINGS_NAV_SHORTCUT_IDS.NAV_PROJECT_SETTINGS_INFRASTRUCTURE]: {
|
||||
id: PROJECT_SETTINGS_NAV_SHORTCUT_IDS.NAV_PROJECT_SETTINGS_INFRASTRUCTURE,
|
||||
label: 'Go to Infrastructure',
|
||||
|
||||
Reference in new issue
Block a user