Files
supabase/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx
Ignacio Dobronich 0d7e14a79c chore: entitlement checks for infra warnings (#43295)
### Changes
- Replace `isFreePlan` with entitlement-based check in infrastructure
warnings: `CPUWarnings`, `RAMWarnings`, and `DiskIOBandwidthWarnings`
previously derived their button label ("Upgrade project" vs "Change
compute add-on") from checking the org's plan. This is now driven by the
`instances.compute_update_available_sizes` entitlement.
2026-03-03 14:07:43 -03:00

444 lines
19 KiB
TypeScript

import { useParams } from 'common'
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 dayjs from 'dayjs'
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 { capitalize } from 'lodash'
import { BarChart2, ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { Fragment, useMemo, useState } from 'react'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { Admonition } from 'ui-patterns/admonition'
import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants'
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>
)}
</ScaffoldSectionContent>
</ScaffoldSection>
</ScaffoldContainer>
<ScaffoldDivider />
</Fragment>
)
})}
</>
)
}